diff --git a/CMakeLists.txt b/CMakeLists.txt index 41deddc..599375c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,7 +126,6 @@ add_qtc_plugin(QodeAssist chat/ChatDocument.hpp chat/ChatDocument.cpp chat/ChatEditor.hpp chat/ChatEditor.cpp chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp - ConfigurationManager.hpp ConfigurationManager.cpp CodeHandler.hpp CodeHandler.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp @@ -166,7 +165,7 @@ add_qtc_plugin(QodeAssist 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) find_program(QtCreatorExecutable diff --git a/ChatView/AgentRoleController.cpp b/ChatView/AgentRoleController.cpp deleted file mode 100644 index af21f53..0000000 --- a/ChatView/AgentRoleController.cpp +++ /dev/null @@ -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 - -#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 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 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 diff --git a/ChatView/AgentRoleController.hpp b/ChatView/AgentRoleController.hpp deleted file mode 100644 index 7eefe67..0000000 --- a/ChatView/AgentRoleController.hpp +++ /dev/null @@ -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 -#include - -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 diff --git a/ChatView/CMakeLists.txt b/ChatView/CMakeLists.txt index b913035..ffdb02e 100644 --- a/ChatView/CMakeLists.txt +++ b/ChatView/CMakeLists.txt @@ -75,8 +75,7 @@ qt_add_qml_module(QodeAssistChatView FileItem.hpp FileItem.cpp ChatFileManager.hpp ChatFileManager.cpp ChatCompressor.hpp ChatCompressor.cpp - AgentRoleController.hpp AgentRoleController.cpp - ChatConfigurationController.hpp ChatConfigurationController.cpp + ChatAgentController.hpp ChatAgentController.cpp FileEditController.hpp FileEditController.cpp InputTokenCounter.hpp InputTokenCounter.cpp ChatHistoryStore.hpp ChatHistoryStore.cpp @@ -92,13 +91,14 @@ target_link_libraries(QodeAssistChatView Qt::Network QtCreator::Core QtCreator::Utils - PluginLLMCore QodeAssistSettings Context QodeAssistUIControlsplugin QodeAssistLogger LLMQore Skills + Agents + Session ) target_include_directories(QodeAssistChatView diff --git a/ChatView/ChatAgentController.cpp b/ChatView/ChatAgentController.cpp new file mode 100644 index 0000000..39309e7 --- /dev/null +++ b/ChatView/ChatAgentController.cpp @@ -0,0 +1,91 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ChatAgentController.hpp" + +#include + +#include + +#include +#include + +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 diff --git a/ChatView/ChatAgentController.hpp b/ChatView/ChatAgentController.hpp new file mode 100644 index 0000000..b175114 --- /dev/null +++ b/ChatView/ChatAgentController.hpp @@ -0,0 +1,47 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +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 m_agentFactory; + QStringList m_availableAgents; + QString m_currentAgent; +}; + +} // namespace QodeAssist::Chat diff --git a/ChatView/ChatCompressor.cpp b/ChatView/ChatCompressor.cpp index e655b33..f062a3d 100644 --- a/ChatView/ChatCompressor.cpp +++ b/ChatView/ChatCompressor.cpp @@ -4,13 +4,21 @@ #include "ChatCompressor.hpp" +#include + #include +#include + #include "ChatModel.hpp" #include "GeneralSettings.hpp" -#include "PromptTemplateManager.hpp" -#include "ProvidersManager.hpp" #include "logger/Logger.hpp" +#include +#include +#include +#include +#include + #include #include #include @@ -25,6 +33,16 @@ ChatCompressor::ChatCompressor(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) { if (m_isCompressing) { @@ -42,20 +60,23 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch return; } - auto providerName = Settings::generalSettings().caProvider(); - m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); - - if (!m_provider) { - emit compressionFailed(tr("No provider available")); + if (!m_sessionManager) { + emit compressionFailed(tr("Chat session manager is not available")); return; } - auto templateName = Settings::generalSettings().caTemplate(); - auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName( - templateName); + QString sessionError; + Session *session = m_sessionManager->createSession(m_activeAgent, &sessionError); + if (!session) { + emit compressionFailed( + sessionError.isEmpty() ? tr("No chat agent selected") : sessionError); + return; + } - if (!promptTemplate) { - emit compressionFailed(tr("No template available")); + auto *client = session->client(); + if (!client) { + m_sessionManager->removeSession(session); + emit compressionFailed(tr("Chat agent has no live client")); return; } @@ -63,23 +84,52 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch m_chatModel = chatModel; m_originalChatPath = chatFilePath; m_accumulatedSummary.clear(); + m_session = session; 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{ - {"model", Settings::generalSettings().caModel()}, {"stream", true}}; + auto *history = session->history(); + 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(msg.content)); + history->append(std::move(apiMessage)); + } - const QString customEndpoint = Settings::generalSettings().caCustomEndpoint(); - const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint - : promptTemplate->endpoint(); - m_provider->client()->setTransferTimeout( + m_connections.append(connect( + client, &::LLMQore::BaseClient::chunkReceived, + this, &ChatCompressor::onPartialResponseReceived, Qt::UniqueConnection)); + 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(Settings::generalSettings().requestTimeout() * 1000)); - m_currentRequestId = m_provider->sendRequest( - QUrl(Settings::generalSettings().caUrl()), payload, endpoint); + + std::vector> blocks; + blocks.push_back(std::make_unique(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)); } @@ -94,10 +144,6 @@ void ChatCompressor::cancelCompression() return; LOG_MESSAGE("Cancelling compression request"); - - if (m_provider && !m_currentRequestId.isEmpty()) - m_provider->cancelRequest(m_currentRequestId); - cleanupState(); emit compressionFailed(tr("Compression cancelled")); } @@ -120,14 +166,18 @@ void ChatCompressor::onFullResponseReceived(const QString &requestId, const QStr LOG_MESSAGE( QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length())); - QString compressedPath = createCompressedChatPath(m_originalChatPath); - if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) { - handleCompressionError(tr("Failed to save compressed chat")); + const QString compressedPath = createCompressedChatPath(m_originalChatPath); + const QString summary = m_accumulatedSummary; + const QString sourcePath = m_originalChatPath; + + cleanupState(); + + if (!createCompressedChatFile(sourcePath, compressedPath, summary)) { + emit compressionFailed(tr("Failed to save compressed chat")); return; } LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath)); - cleanupState(); emit compressionCompleted(compressedPath); } @@ -168,39 +218,6 @@ QString ChatCompressor::buildCompressionPrompt() const "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 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( const QString &sourcePath, const QString &destPath, const QString &summary) { @@ -247,32 +264,6 @@ bool ChatCompressor::createCompressedChatFile( 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() { for (const auto &connection : std::as_const(m_connections)) @@ -284,12 +275,17 @@ void ChatCompressor::cleanupState() { disconnectAllSignals(); + Session *session = m_session; + m_isCompressing = false; m_currentRequestId.clear(); m_originalChatPath.clear(); m_accumulatedSummary.clear(); m_chatModel = nullptr; - m_provider = nullptr; + m_session = nullptr; + + if (session && m_sessionManager) + m_sessionManager->removeSession(session); } } // namespace QodeAssist::Chat diff --git a/ChatView/ChatCompressor.hpp b/ChatView/ChatCompressor.hpp index d9587f9..8eb3641 100644 --- a/ChatView/ChatCompressor.hpp +++ b/ChatView/ChatCompressor.hpp @@ -4,15 +4,15 @@ #pragma once -#include #include #include +#include #include -namespace QodeAssist::PluginLLMCore { -class Provider; -class PromptTemplate; -} // namespace QodeAssist::PluginLLMCore +namespace QodeAssist { +class SessionManager; +class Session; +} namespace QodeAssist::Chat { @@ -25,6 +25,9 @@ class ChatCompressor : public QObject public: explicit ChatCompressor(QObject *parent = nullptr); + void setSessionManager(SessionManager *sessionManager); + void setActiveAgent(const QString &agentName); + void startCompression(const QString &chatFilePath, ChatModel *chatModel); bool isCompressing() const; @@ -45,17 +48,17 @@ private: QString buildCompressionPrompt() const; bool createCompressedChatFile( const QString &sourcePath, const QString &destPath, const QString &summary); - void connectProviderSignals(); void disconnectAllSignals(); void cleanupState(); void handleCompressionError(const QString &error); - void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate); bool m_isCompressing = false; QString m_currentRequestId; QString m_originalChatPath; QString m_accumulatedSummary; - PluginLLMCore::Provider *m_provider = nullptr; + QPointer m_sessionManager; + QString m_activeAgent; + QPointer m_session; ChatModel *m_chatModel = nullptr; QList m_connections; diff --git a/ChatView/ChatConfigurationController.cpp b/ChatView/ChatConfigurationController.cpp deleted file mode 100644 index fd4673c..0000000 --- a/ChatView/ChatConfigurationController.cpp +++ /dev/null @@ -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 - -#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 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 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 diff --git a/ChatView/ChatConfigurationController.hpp b/ChatView/ChatConfigurationController.hpp deleted file mode 100644 index bb4ec1e..0000000 --- a/ChatView/ChatConfigurationController.hpp +++ /dev/null @@ -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 -#include - -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 diff --git a/ChatView/ChatRootView.cpp b/ChatView/ChatRootView.cpp index d040ec2..c86ad12 100644 --- a/ChatView/ChatRootView.cpp +++ b/ChatView/ChatRootView.cpp @@ -28,9 +28,11 @@ #include "QodeAssistConstants.hpp" -#include "AgentRoleController.hpp" +#include +#include + +#include "ChatAgentController.hpp" #include "ChatAssistantSettings.hpp" -#include "ChatConfigurationController.hpp" #include "ChatCompressor.hpp" #include "ChatHistoryStore.hpp" #include "FileEditController.hpp" @@ -38,10 +40,8 @@ #include "InputTokenCounter.hpp" #include "SettingsConstants.hpp" #include "Logger.hpp" -#include "ProvidersManager.hpp" #include "SessionFileRegistry.hpp" #include "context/ContextManager.hpp" -#include "pluginllmcore/RulesLoader.hpp" #include "ProjectSettings.hpp" #include "SkillsSettings.hpp" #include "sources/skills/SkillsManager.hpp" @@ -74,13 +74,11 @@ QKeySequence sendMessageKeySequence() ChatRootView::ChatRootView(QQuickItem *parent) : QQuickItem(parent) , m_chatModel(new ChatModel(this)) - , m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) - , m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this)) + , m_clientInterface(new ClientInterface(m_chatModel, this)) , m_fileManager(new ChatFileManager(this)) , m_isRequestInProgress(false) , m_chatCompressor(new ChatCompressor(this)) - , m_agentRoleController(new AgentRoleController(this)) - , m_configurationController(new ChatConfigurationController(this)) + , m_agentController(new ChatAgentController(this)) , m_fileEditController(new FileEditController(m_chatModel, this)) , m_tokenCounter( new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this)) @@ -109,22 +107,6 @@ ChatRootView::ChatRootView(QQuickItem *parent) }, 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( m_clientInterface, &ClientInterface::messageReceivedCompletely, @@ -171,20 +153,20 @@ ChatRootView::ChatRootView(QQuickItem *parent) this, &ChatRootView::inputTokensCountChanged); connect( - m_agentRoleController, - &AgentRoleController::availableRolesChanged, + m_agentController, + &ChatAgentController::availableAgentsChanged, this, - &ChatRootView::availableAgentRolesChanged); + &ChatRootView::availableChatAgentsChanged); connect( - m_agentRoleController, - &AgentRoleController::currentRoleChanged, + m_agentController, + &ChatAgentController::currentAgentChanged, this, - &ChatRootView::currentAgentRoleChanged); + &ChatRootView::currentChatAgentChanged); connect( - m_agentRoleController, - &AgentRoleController::baseSystemPromptChanged, + m_agentController, + &ChatAgentController::currentAgentChanged, this, - &ChatRootView::baseSystemPromptChanged); + &ChatRootView::isThinkingSupportChanged); auto editors = Core::EditorManager::instance(); @@ -266,14 +248,6 @@ ChatRootView::ChatRootView(QQuickItem *parent) connect( m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory); - refreshRules(); - - connect( - ProjectExplorer::ProjectManager::instance(), - &ProjectExplorer::ProjectManager::startupProjectChanged, - this, - &ChatRootView::refreshRules); - connect( ProjectExplorer::ProjectManager::instance(), &ProjectExplorer::ProjectManager::projectAdded, @@ -298,12 +272,6 @@ ChatRootView::ChatRootView(QQuickItem *parent) this, &ChatRootView::useThinkingChanged); - connect( - &Settings::generalSettings().caProvider, - &Utils::BaseAspect::changed, - this, - &ChatRootView::isThinkingSupportChanged); - connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) { m_lastErrorMessage = error; emit lastErrorMessageChanged(); @@ -373,6 +341,48 @@ Skills::SkillsManager *ChatRootView::skillsManager() const return m_skillsManager; } +AgentFactory *ChatRootView::agentFactory() const +{ + if (!m_agentFactory) { + if (auto *engine = qmlEngine(this)) { + m_agentFactory = qobject_cast( + engine->rootContext()->contextProperty("agentFactory").value()); + } + } + return m_agentFactory; +} + +SessionManager *ChatRootView::sessionManager() const +{ + if (!m_sessionManager) { + if (auto *engine = qmlEngine(this)) { + m_sessionManager = qobject_cast( + engine->rootContext()->contextProperty("sessionManager").value()); + } + } + 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 results; @@ -380,7 +390,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const if (!manager || !Settings::skillsSettings().enableSkills()) return results; - auto *project = PluginLLMCore::RulesLoader::getActiveProject(); + auto *project = ProjectExplorer::ProjectManager::startupProject(); QStringList projectSkillDirs; if (project) { Settings::ProjectSettings projectSettings(project); @@ -481,7 +491,12 @@ void ChatRootView::dispatchSend( m_tokenCounter->recordSent(); + if (currentChatAgent().isEmpty()) + loadAvailableChatAgents(); + m_clientInterface->setSkillsManager(skillsManager()); + m_clientInterface->setSessionManager(sessionManager()); + m_clientInterface->setActiveAgent(currentChatAgent()); m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg); m_fileManager->clearIntermediateStorage(); @@ -527,12 +542,6 @@ void ChatRootView::clearMessages() clearLinkedFiles(); } -QString ChatRootView::currentTemplate() const -{ - auto &settings = Settings::generalSettings(); - return settings.caModel(); -} - void ChatRootView::saveHistory(const QString &filePath) { if (filePath != m_recentFilePath) { @@ -821,25 +830,6 @@ void ChatRootView::openChatHistoryFolder() 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() { QMetaObject::invokeMethod( @@ -1120,51 +1110,6 @@ QString ChatRootView::lastErrorMessage() const 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 { return Settings::chatAssistantSettings().enableChatTools(); @@ -1249,10 +1194,7 @@ QString ChatRootView::lastInfoMessage() const bool ChatRootView::isThinkingSupport() const { - auto providerName = Settings::generalSettings().caProvider(); - auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); - - return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking); + return m_agentController->currentSupportsThinking(); } bool ChatRootView::hasImageAttachments(const QStringList &attachments) const @@ -1273,66 +1215,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const 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() { if (m_chatCompressor->isCompressing()) { @@ -1349,6 +1231,10 @@ void ChatRootView::compressCurrentChat() autosave(); + if (currentChatAgent().isEmpty()) + loadAvailableChatAgents(); + m_chatCompressor->setSessionManager(sessionManager()); + m_chatCompressor->setActiveAgent(currentChatAgent()); m_chatCompressor->startCompression(m_recentFilePath, m_chatModel); } diff --git a/ChatView/ChatRootView.hpp b/ChatView/ChatRootView.hpp index 7403872..ddd0299 100644 --- a/ChatView/ChatRootView.hpp +++ b/ChatView/ChatRootView.hpp @@ -11,18 +11,21 @@ #include "ChatFileManager.hpp" #include "ChatModel.hpp" #include "ClientInterface.hpp" -#include "pluginllmcore/PromptProviderChat.hpp" #include namespace QodeAssist::Skills { class SkillsManager; } +namespace QodeAssist { +class AgentFactory; +class SessionManager; +} + namespace QodeAssist::Chat { class ChatCompressor; -class AgentRoleController; -class ChatConfigurationController; +class ChatAgentController; class FileEditController; class InputTokenCounter; class ChatHistoryStore; @@ -32,7 +35,6 @@ class ChatRootView : public QQuickItem { Q_OBJECT 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(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged 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(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged 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 useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged 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 currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) - Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) - Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged 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(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL) + Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL) Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL) @@ -75,7 +70,6 @@ public: ~ChatRootView() override; ChatModel *chatModel() const; - QString currentTemplate() const; void saveHistory(const QString &filePath); void loadHistory(const QString &filePath); @@ -104,7 +98,6 @@ public: QString sendShortcutText() const; Q_INVOKABLE void setIsSyncOpenFiles(bool state); Q_INVOKABLE void openChatHistoryFolder(); - Q_INVOKABLE void openRulesFolder(); Q_INVOKABLE void openSettings(); Q_INVOKABLE void openFileInEditor(const QString &filePath); @@ -139,11 +132,6 @@ public: void setRequestProgressStatus(bool state); 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; @@ -161,23 +149,14 @@ public: Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); 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 cancelCompression(); - Q_INVOKABLE void loadAvailableAgentRoles(); - Q_INVOKABLE void applyAgentRole(const QString &roleId); - Q_INVOKABLE void openAgentRolesSettings(); - QStringList availableAgentRoles() const; - QString currentAgentRole() const; - QString baseSystemPrompt() const; - QString currentAgentRoleDescription() const; - QString currentAgentRoleSystemPrompt() const; - + Q_INVOKABLE void loadAvailableChatAgents(); + QStringList availableChatAgents() const; + QString currentChatAgent() const; + void setCurrentChatAgent(const QString &name); + int currentMessageTotalEdits() const; int currentMessageAppliedEdits() const; int currentMessagePendingEdits() const; @@ -206,7 +185,6 @@ public slots: signals: void chatModelChanged(); - void currentTemplateChanged(); void attachmentFilesChanged(); void linkedFilesChanged(); void inputTokensCountChanged(); @@ -223,20 +201,15 @@ signals: void lastErrorMessageChanged(); void lastInfoMessageChanged(); void sendShortcutTextChanged(); - void activeRulesChanged(); - void activeRulesCountChanged(); void useToolsChanged(); void useThinkingChanged(); void currentMessageEditsStatsChanged(); void isThinkingSupportChanged(); - void availableConfigurationsChanged(); - void currentConfigurationChanged(); - void availableAgentRolesChanged(); - void currentAgentRoleChanged(); - void baseSystemPromptChanged(); + void availableChatAgentsChanged(); + void currentChatAgentChanged(); void isCompressingChanged(); void compressionCompleted(const QString &compressedChatPath); @@ -269,12 +242,12 @@ private: SessionFileRegistry *sessionFileRegistry() const; Skills::SkillsManager *skillsManager() const; + AgentFactory *agentFactory() const; + SessionManager *sessionManager() const; ChatModel *m_chatModel; - PluginLLMCore::PromptProviderChat m_promptProvider; ClientInterface *m_clientInterface; ChatFileManager *m_fileManager; - QString m_currentTemplate; QString m_recentFilePath; QStringList m_attachmentFiles; QStringList m_linkedFiles; @@ -294,13 +267,11 @@ private: QList m_currentEditors; bool m_isRequestInProgress; QString m_lastErrorMessage; - QVariantList m_activeRules; - + QString m_lastInfoMessage; ChatCompressor *m_chatCompressor; - AgentRoleController *m_agentRoleController; - ChatConfigurationController *m_configurationController; + ChatAgentController *m_agentController; FileEditController *m_fileEditController; InputTokenCounter *m_tokenCounter; ChatHistoryStore *m_historyStore; @@ -308,6 +279,8 @@ private: mutable bool m_sessionFileRegistryResolved = false; mutable QPointer m_skillsManager; mutable bool m_skillsManagerResolved = false; + mutable QPointer m_agentFactory; + mutable QPointer m_sessionManager; }; } // namespace QodeAssist::Chat diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 365233f..1cac885 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -4,65 +4,75 @@ #include "ClientInterface.hpp" -#include +#include +#include +#include +#include + +#include +#include +#include #include +#include +#include #include #include +#include + #include #include -#include #include #include #include #include #include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include +#include +#include +#include +#include +#include #include "tools/ReadOriginalHistoryTool.hpp" #include "tools/TodoTool.hpp" +#include "tools/ToolsRegistration.hpp" -#include "ChatAssistantSettings.hpp" #include "ChatSerializer.hpp" #include "GeneralSettings.hpp" #include "Logger.hpp" #include "ProjectSettings.hpp" -#include "ProvidersManager.hpp" #include "SkillsSettings.hpp" #include "ToolsSettings.hpp" -#include #include #include namespace QodeAssist::Chat { -ClientInterface::ClientInterface( - ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent) +ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent) : QObject(parent) - , m_promptProvider(promptProvider) , m_chatModel(chatModel) , m_contextManager(new Context::ContextManager(this)) {} +ClientInterface::~ClientInterface() +{ + cancelRequest(); +} + void ClientInterface::setSkillsManager(Skills::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( @@ -72,6 +82,8 @@ void ClientInterface::sendMessage( bool useTools, bool useThinking) { + Q_UNUSED(useThinking) + if (message.trimmed().isEmpty() && attachments.isEmpty()) { LOG_MESSAGE("Ignoring empty chat message"); return; @@ -84,13 +96,11 @@ void ClientInterface::sendMessage( QList imageFiles; QList textFiles; - for (const QString &filePath : attachments) { - if (isImageFile(filePath)) { + if (isImageFile(filePath)) imageFiles.append(filePath); - } else { + else textFiles.append(filePath); - } } QList storedAttachments; @@ -116,9 +126,8 @@ void ClientInterface::sendMessage( if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { for (const QString &imagePath : imageFiles) { QString base64Data = encodeImageToBase64(imagePath); - if (base64Data.isEmpty()) { + if (base64Data.isEmpty()) continue; - } QString storedPath; QFileInfo fileInfo(imagePath); @@ -129,7 +138,6 @@ void ClientInterface::sendMessage( imageAttachment.storedPath = storedPath; imageAttachment.mediaType = getMediaTypeForImage(imagePath); imageAttachments.append(imageAttachment); - LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath)); } } @@ -138,318 +146,302 @@ void ClientInterface::sendMessage( .arg(imageFiles.size())); } - m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments); - - auto &chatAssistantSettings = Settings::chatAssistantSettings(); - - 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)); + if (!m_sessionManager) { + const QString error = QStringLiteral("Chat session manager is not available"); + LOG_MESSAGE(error); + emit errorOccurred(error); return; } - auto templateName = Settings::generalSettings().caTemplate(); - auto promptTemplate = m_promptProvider->getTemplateByName(templateName); + // Snapshot prior turns BEFORE the new user message is appended to the model. + const QVector priorHistory = m_chatModel->getChatHistory(); - if (!promptTemplate) { - LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); + m_chatModel + ->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; } - PluginLLMCore::ContextData context; - - const bool isToolsEnabled = useTools; - - if (chatAssistantSettings.useSystemPrompt()) { - QString systemPrompt = chatAssistantSettings.systemPrompt(); - - 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; + auto *client = session->client(); + if (!client) { + const QString error = QStringLiteral("Chat agent has no live client"); + LOG_MESSAGE(error); + m_sessionManager->removeSession(session); + emit errorOccurred(error); + return; } - const bool toolHistory = promptTemplate->supportsToolHistory(); - - QVector messages; - int toolCallMsgIdx = -1; - for (const auto &msg : m_chatModel->getChatHistory()) { - 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( + Tools::registerQodeAssistTools(client->tools()); + if (m_skillsManager) + Tools::registerSkillTool(client->tools(), m_skillsManager); + client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations()); + client->setTransferTimeout( static_cast(Settings::generalSettings().requestTimeout() * 1000)); - connect( - provider->client(), - &::LLMQore::BaseClient::chunkReceived, - 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 chatContext = buildChatContextLayer(message, linkedFiles); + if (!chatContext.isEmpty()) + session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext); - const QString customEndpoint = Settings::generalSettings().caCustomEndpoint(); - const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint - : promptTemplate->endpoint(); - auto requestId - = provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint); - QJsonObject request{{"id", requestId}}; + seedHistory(*session->history(), priorHistory); - 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); - - if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools) - && provider->toolsManager()) { - if (auto *todoTool = qobject_cast( - provider->toolsManager()->tool("todo_tool"))) { + std::vector> blocks; + blocks.push_back(std::make_unique(userText)); + + if (!imageAttachments.isEmpty() && session->supportsImages() && !m_chatFilePath.isEmpty()) { + for (const auto &image : imageAttachments) { + QString base64 + = ChatSerializer::loadContentFromStorage(m_chatFilePath, image.storedPath); + if (base64.isEmpty()) + continue; + blocks.push_back(std::make_unique( + 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(client->tools()->tool("todo_tool"))) { todoTool->setCurrentSessionId(m_chatFilePath); } if (auto *historyTool = qobject_cast( - provider->toolsManager()->tool("read_original_history"))) { + client->tools()->tool("read_original_history"))) { 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 &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( + toolMsg.id, toolMsg.toolName, toolMsg.toolArguments)); + toolResults.appendBlock( + std::make_unique(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(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( + 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(msg.content)); + history.append(std::move(assistant)); + } + } } -void ClientInterface::clearMessages() +QString ClientInterface::buildChatContextLayer( + const QString &message, const QList &linkedFiles) const { - const auto providerName = Settings::generalSettings().caProvider(); - auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); + QString context; - if (provider && !m_chatFilePath.isEmpty() - && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools) - && provider->toolsManager()) { - if (auto *todoTool = qobject_cast( - provider->toolsManager()->tool("todo_tool"))) { - todoTool->clearSession(m_chatFilePath); + auto *project = ProjectExplorer::ProjectManager::startupProject(); + if (project) { + context += QString("# Active project: %1").arg(project->displayName()); + context += 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()) { + 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(); } void ClientInterface::cancelRequest() { - QSet providers; - 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()); - } - } - + const auto requests = m_activeRequests; m_activeRequests.clear(); m_accumulatedResponses.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) @@ -486,23 +478,6 @@ QString ClientInterface::getCurrentFileContext() const return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content); } -QString ClientInterface::getSystemPromptWithLinkedFiles( - const QString &basePrompt, const QList &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 { return m_contextManager; @@ -532,7 +507,8 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString if (it == m_activeRequests.end()) 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]; @@ -546,13 +522,16 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString } LOG_MESSAGE( - "Message completed. Final response for message " + ctx.originalRequest["id"].toString() - + ": " + finalText); + "Message completed. Final response for message " + originalRequest["id"].toString() + ": " + + finalText); emit messageReceivedCompletely(); m_activeRequests.erase(it); m_accumulatedResponses.remove(requestId); m_awaitingContinuation.remove(requestId); + + if (session && m_sessionManager) + m_sessionManager->removeSession(session); } void ClientInterface::handleRequestFinalized( @@ -584,12 +563,17 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin if (it == m_activeRequests.end()) return; + Session *session = it.value().session; + LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error)); emit errorOccurred(error); m_activeRequests.erase(it); m_accumulatedResponses.remove(requestId); m_awaitingContinuation.remove(requestId); + + if (session && m_sessionManager) + m_sessionManager->removeSession(session); } void ClientInterface::handleThinkingBlockReceived( @@ -693,46 +677,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const return imageData.toBase64(); } -QVector ClientInterface::loadImagesFromStorage( - const QList &storedImages) const -{ - QVector 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) { - 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( - provider->toolsManager()->tool("todo_tool"))) { - todoTool->clearSession(m_chatFilePath); - } - } - } - m_chatFilePath = filePath; m_chatModel->setChatFilePath(filePath); } diff --git a/ChatView/ClientInterface.hpp b/ChatView/ClientInterface.hpp index 843a706..7dc6221 100644 --- a/ChatView/ClientInterface.hpp +++ b/ChatView/ClientInterface.hpp @@ -5,16 +5,21 @@ #pragma once #include +#include #include #include #include #include "ChatModel.hpp" -#include "Provider.hpp" -#include "pluginllmcore/IPromptProvider.hpp" #include #include +namespace QodeAssist { +class SessionManager; +class Session; +class ConversationHistory; +} + namespace QodeAssist::Skills { class SkillsManager; } @@ -26,11 +31,12 @@ class ClientInterface : public QObject Q_OBJECT public: - explicit ClientInterface( - ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr); + explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr); ~ClientInterface(); void setSkillsManager(Skills::SkillsManager *skillsManager); + void setSessionManager(SessionManager *sessionManager); + void setActiveAgent(const QString &agentName); void sendMessage( const QString &message, @@ -42,7 +48,7 @@ public: void cancelRequest(); Context::ContextManager *contextManager() const; - + void setChatFilePath(const QString &filePath); QString chatFilePath() const; @@ -74,24 +80,26 @@ private slots: private: void handleLLMResponse(const QString &response, const QJsonObject &request); QString getCurrentFileContext() const; - QString getSystemPromptWithLinkedFiles( - const QString &basePrompt, const QList &linkedFiles) const; + QString buildChatContextLayer( + const QString &message, const QList &linkedFiles) const; + void seedHistory( + ConversationHistory &history, const QVector &messages) const; bool isImageFile(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const; - QVector loadImagesFromStorage(const QList &storedImages) const; struct RequestContext { QJsonObject originalRequest; - PluginLLMCore::Provider *provider; + QPointer session; bool dropPreToolText = false; }; - PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; ChatModel *m_chatModel; Context::ContextManager *m_contextManager; Skills::SkillsManager *m_skillsManager = nullptr; + QPointer m_sessionManager; + QString m_activeAgent; QString m_chatFilePath; QHash m_activeRequests; diff --git a/ChatView/InputTokenCounter.cpp b/ChatView/InputTokenCounter.cpp index dd0d395..b59a1ea 100644 --- a/ChatView/InputTokenCounter.cpp +++ b/ChatView/InputTokenCounter.cpp @@ -6,17 +6,11 @@ #include -#include -#include -#include - #include #include "ChatAssistantSettings.hpp" #include "ChatModel.hpp" -#include "GeneralSettings.hpp" #include "Logger.hpp" -#include "ProvidersManager.hpp" #include "context/ContextManager.hpp" #include "context/TokenUtils.hpp" @@ -42,12 +36,6 @@ InputTokenCounter::InputTokenCounter( this, &InputTokenCounter::recompute); - connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() { - rewireToolsChangedConnection(); - recompute(); - }); - - rewireToolsChangedConnection(); recompute(); } @@ -74,24 +62,6 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles) 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() { int inputTokens = m_messageTokens; @@ -136,21 +106,6 @@ void InputTokenCounter::recompute() 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(serialized.size() / 4); - } - } - } - } - m_inputTokens = static_cast(inputTokens * m_calibrationFactor); emit inputTokensChanged(); } diff --git a/ChatView/InputTokenCounter.hpp b/ChatView/InputTokenCounter.hpp index 2e1ac83..86260f1 100644 --- a/ChatView/InputTokenCounter.hpp +++ b/ChatView/InputTokenCounter.hpp @@ -37,11 +37,8 @@ signals: void inputTokensChanged(); private: - void rewireToolsChangedConnection(); - ChatModel *m_chatModel; Context::ContextManager *m_contextManager; - QMetaObject::Connection m_toolsChangedConn; QStringList m_attachments; QStringList m_linkedFiles; diff --git a/ChatView/qml/RootItem.qml b/ChatView/qml/RootItem.qml index 585af60..e0e5f3b 100644 --- a/ChatView/qml/RootItem.qml +++ b/ChatView/qml/RootItem.qml @@ -152,29 +152,17 @@ ChatRootView { } } settingsButton.onClicked: root.openSettings() - configSelector { - model: root.availableConfigurations - displayText: root.currentConfiguration + agentSelector { + model: root.availableChatAgents + displayText: root.currentChatAgent onActivated: function(index) { - if (index > 0) { - root.applyConfiguration(root.availableConfigurations[index]) - } + root.currentChatAgent = root.availableChatAgents[index] } + Component.onCompleted: root.loadAvailableChatAgents() + popup.onAboutToShow: { - root.loadAvailableConfigurations() - } - } - - roleSelector { - model: root.availableAgentRoles - displayText: root.currentAgentRole - onActivated: function(index) { - root.applyAgentRole(root.availableAgentRoles[index]) - } - - popup.onAboutToShow: { - root.loadAvailableAgentRoles() + root.loadAvailableChatAgents() } } } @@ -839,20 +827,7 @@ ChatRootView { x: (parent.width - width) / 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() - onOpenAgentRolesSettings: root.openAgentRolesSettings() - onOpenRulesFolder: root.openRulesFolder() - onRefreshRules: root.refreshRules() - onRuleSelected: function(index) { - contextViewer.selectedRuleContent = root.getRuleContent(index) - } } Connections { diff --git a/ChatView/qml/controls/TopBar.qml b/ChatView/qml/controls/TopBar.qml index efe792e..27a1bab 100644 --- a/ChatView/qml/controls/TopBar.qml +++ b/ChatView/qml/controls/TopBar.qml @@ -26,8 +26,7 @@ Rectangle { property alias toolsButton: toolsButtonId property alias thinkingMode: thinkingModeId property alias settingsButton: settingsButtonId - property alias configSelector: configSelectorId - property alias roleSelector: roleSelector + property alias agentSelector: agentSelectorId property alias relocateTooltip: relocateTooltipId color: palette.window.hslLightness > 0.5 ? @@ -134,7 +133,7 @@ Rectangle { } QoAComboBox { - id: configSelectorId + id: agentSelectorId implicitHeight: 25 @@ -142,24 +141,9 @@ Rectangle { currentIndex: 0 QoAToolTip { - visible: configSelectorId.hovered + visible: agentSelectorId.hovered delay: 250 - text: qsTr("Switch saved AI configuration") - } - } - - QoAComboBox { - id: roleSelector - - implicitHeight: 25 - - model: [] - currentIndex: 0 - - QoAToolTip { - visible: roleSelector.hovered - delay: 250 - text: qsTr("Switch agent role (different system prompts)") + text: qsTr("Select chat agent (provider, model and role come from the agent)") } } } diff --git a/ConfigurationManager.cpp b/ConfigurationManager.cpp deleted file mode 100644 index 4bdfc18..0000000 --- a/ConfigurationManager.cpp +++ /dev/null @@ -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 -#include - -#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(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(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 &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(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(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 diff --git a/ConfigurationManager.hpp b/ConfigurationManager.hpp deleted file mode 100644 index 0cd10b1..0000000 --- a/ConfigurationManager.hpp +++ /dev/null @@ -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 - -#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 diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index 4938f4e..38b9515 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -9,27 +9,39 @@ #include #include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include "sources/common/ContextData.hpp" + #include "CodeHandler.hpp" #include "context/DocumentContextReader.hpp" #include "context/Utils.hpp" #include "logger/Logger.hpp" #include "settings/CodeCompletionSettings.hpp" #include "settings/GeneralSettings.hpp" -#include +#include "sources/settings/PipelinesConfig.hpp" namespace QodeAssist { LLMClientInterface::LLMClientInterface( const Settings::GeneralSettings &generalSettings, const Settings::CodeCompletionSettings &completeSettings, - PluginLLMCore::IProviderRegistry &providerRegistry, - PluginLLMCore::IPromptProvider *promptProvider, + SessionManager &sessionManager, + AgentFactory &agentFactory, Context::IDocumentReader &documentReader, IRequestPerformanceLogger &performanceLogger) : m_generalSettings(generalSettings) , m_completeSettings(completeSettings) - , m_providerRegistry(providerRegistry) - , m_promptProvider(promptProvider) + , m_sessionManager(sessionManager) + , m_agentFactory(agentFactory) , m_documentReader(documentReader) , m_performanceLogger(performanceLogger) , m_contextManager(new Context::ContextManager(this)) @@ -51,58 +63,64 @@ void LLMClientInterface::startImpl() 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); if (it == m_activeRequests.end()) return; - const RequestContext &ctx = it.value(); - sendCompletionToClient(fullText, ctx.originalRequest, true); - - m_activeRequests.erase(it); - m_performanceLogger.endTimeMeasurement(requestId); + if (event.kind() == ResponseEvent::Kind::TextDelta) { + if (const auto *delta = event.as()) + it.value().accumulated += delta->text; + } } -void LLMClientInterface::handleRequestFinalized( - 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) +void LLMClientInterface::onSessionFinished(const QString &requestId) { auto it = m_activeRequests.find(requestId); if (it == m_activeRequests.end()) 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)); - // Send LSP error response to client QJsonObject response; response["jsonrpc"] = "2.0"; - response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"]; + response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"]; QJsonObject errorObject; errorObject["code"] = -32603; // Internal error code errorObject["message"] = error; response["error"] = errorObject; - + 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_performanceLogger.endTimeMeasurement(requestId); + + if (session) + m_sessionManager.removeSession(session); } void LLMClientInterface::sendData(const QByteArray &data) @@ -135,26 +153,15 @@ void LLMClientInterface::sendData(const QByteArray &data) void LLMClientInterface::handleCancelRequest() { - QSet providers; - 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()); - } - } - + const auto requests = m_activeRequests; 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"); } @@ -237,133 +244,86 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) return; } - auto updatedContext = prepareContext(request, documentInfo); - - bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo); - - 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); + const QString agentName = pickCompletionAgent(filePath); + if (agentName.isEmpty()) { + QString error = QString("No code completion agent matches: %1").arg(filePath); LOG_MESSAGE(error); sendErrorResponse(request, error); return; } - auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() - : m_generalSettings.ccPreset1Template(); + QString sessionError; + 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 error = QString("No template found with name: %1").arg(templateName); + QString editorContext; + 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(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); sendErrorResponse(request, error); return; } - QJsonObject payload{{"model", modelName}, {"stream", true}}; - - 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(); - } - 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 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(m_generalSettings.requestTimeout() * 1000)); - - auto requestId - = provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active)); - m_activeRequests[requestId] = {request, provider}; + m_activeRequests[requestId] = {request, session, QString()}; 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) { QJsonObject params = request["params"].toObject(); @@ -374,15 +334,14 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext( Context::DocumentContextReader 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( - PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const -{ - const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint() - : m_generalSettings.ccCustomEndpoint(); - return !custom.isEmpty() ? custom : promptTemplate->endpoint(); + Templates::ContextData context; + context.prefix = legacy.prefix; + context.suffix = legacy.suffix; + context.fileContext = legacy.fileContext; + return context; } Context::ContextManager *LLMClientInterface::contextManager() const @@ -393,15 +352,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const void LLMClientInterface::sendCompletionToClient( 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 response; diff --git a/LLMClientInterface.hpp b/LLMClientInterface.hpp index 5eb223e..d51c0f4 100644 --- a/LLMClientInterface.hpp +++ b/LLMClientInterface.hpp @@ -8,12 +8,11 @@ #include #include +#include + #include #include #include -#include -#include -#include #include #include #include @@ -23,6 +22,15 @@ class QNetworkAccessManager; namespace QodeAssist { +class SessionManager; +class AgentFactory; +class Session; +class ResponseEvent; + +namespace Templates { +struct ContextData; +} + class LLMClientInterface : public LanguageClient::BaseClientInterface { Q_OBJECT @@ -31,8 +39,8 @@ public: LLMClientInterface( const Settings::GeneralSettings &generalSettings, const Settings::CodeCompletionSettings &completeSettings, - PluginLLMCore::IProviderRegistry &providerRegistry, - PluginLLMCore::IPromptProvider *promptProvider, + SessionManager &sessionManager, + AgentFactory &agentFactory, Context::IDocumentReader &documentReader, IRequestPerformanceLogger &performanceLogger); ~LLMClientInterface() override; @@ -52,12 +60,6 @@ public: protected: 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: void handleInitialize(const QJsonObject &request); void handleShutdown(const QJsonObject &request); @@ -67,22 +69,28 @@ private: void handleCancelRequest(); 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 { QJsonObject originalRequest; - PluginLLMCore::Provider *provider; + QPointer session; + QString accumulated; }; - PluginLLMCore::ContextData prepareContext( + Templates::ContextData prepareContext( const QJsonObject &request, const Context::DocumentInfo &documentInfo); - QString resolveEndpoint( - PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const; + QString pickCompletionAgent(const QString &filePath) const; const Settings::CodeCompletionSettings &m_completeSettings; const Settings::GeneralSettings &m_generalSettings; - PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; - PluginLLMCore::IProviderRegistry &m_providerRegistry; + SessionManager &m_sessionManager; + AgentFactory &m_agentFactory; Context::IDocumentReader &m_documentReader; IRequestPerformanceLogger &m_performanceLogger; QElapsedTimer m_completionTimer; diff --git a/QodeAssistClient.cpp b/QodeAssistClient.cpp index 4980632..34a1616 100644 --- a/QodeAssistClient.cpp +++ b/QodeAssistClient.cpp @@ -159,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) m_refactorWidgetHandler = new RefactorWidgetHandler(this); } +void QodeAssistClient::setSessionManager(SessionManager *sessionManager) +{ + m_sessionManager = sessionManager; +} + +void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory) +{ + m_agentFactory = agentFactory; +} + QodeAssistClient::~QodeAssistClient() { cleanupConnections(); @@ -319,6 +329,8 @@ void QodeAssistClient::requestQuickRefactor( if (!m_refactorHandler) { m_refactorHandler = new QuickRefactorHandler(this); + m_refactorHandler->setSessionManager(m_sessionManager); + m_refactorHandler->setAgentFactory(m_agentFactory); connect( m_refactorHandler, &QuickRefactorHandler::refactoringCompleted, diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index d828ec1..de2e688 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -6,6 +6,7 @@ #pragma once #include +#include #include "LLMClientInterface.hpp" #include "LSPCompletion.hpp" @@ -21,6 +22,9 @@ namespace QodeAssist { +class SessionManager; +class AgentFactory; + class QodeAssistClient : public LanguageClient::Client { Q_OBJECT @@ -28,6 +32,9 @@ public: explicit QodeAssistClient(LLMClientInterface *clientInterface); ~QodeAssistClient() override; + void setSessionManager(SessionManager *sessionManager); + void setAgentFactory(AgentFactory *agentFactory); + void openDocument(TextEditor::TextDocument *document) override; bool canOpenProject(ProjectExplorer::Project *project) override; @@ -68,6 +75,8 @@ private: RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; RefactorWidgetHandler *m_refactorWidgetHandler{nullptr}; LLMClientInterface *m_llmClient; + SessionManager *m_sessionManager{nullptr}; + AgentFactory *m_agentFactory{nullptr}; }; } // namespace QodeAssist diff --git a/QuickRefactorHandler.cpp b/QuickRefactorHandler.cpp index 3eccf10..a8cb0c6 100644 --- a/QuickRefactorHandler.cpp +++ b/QuickRefactorHandler.cpp @@ -4,24 +4,38 @@ #include "QuickRefactorHandler.hpp" +#include + #include +#include +#include #include #include #include +#include +#include +#include + #include -#include #include #include -#include -#include -#include #include -#include -#include +#include #include #include +#include "sources/common/ContextData.hpp" + +#include +#include +#include +#include +#include + +#include "sources/settings/PipelinesConfig.hpp" +#include "tools/ToolsRegistration.hpp" + namespace QodeAssist { QuickRefactorHandler::QuickRefactorHandler(QObject *parent) @@ -34,6 +48,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent) QuickRefactorHandler::~QuickRefactorHandler() {} +void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager) +{ + m_sessionManager = sessionManager; +} + +void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory) +{ + m_agentFactory = agentFactory; +} + void QuickRefactorHandler::sendRefactorRequest( TextEditor::TextEditorWidget *editor, const QString &instructions) { @@ -88,61 +112,70 @@ void QuickRefactorHandler::sendRefactorRequest( 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( TextEditor::TextEditorWidget *editor, const QString &instructions, const Utils::Text::Range &range) { - auto &settings = Settings::generalSettings(); - - 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); + const auto emitError = [this, editor](const QString &error) { LOG_MESSAGE(error); RefactorResult result; result.success = false; result.errorMessage = error; result.editor = editor; emit refactoringCompleted(result); + }; + + if (!m_sessionManager) { + emitError(QStringLiteral("Quick refactor session manager is not available")); return; } - const auto templateName = settings.qrTemplate(); - auto promptTemplate = promptManager.getChatTemplateByName(templateName); - - if (!promptTemplate) { - 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); + const QString filePath = editor->textDocument()->filePath().toUrlishString(); + const QString agentName = pickRefactorAgent(filePath); + if (agentName.isEmpty()) { + emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath)); return; } - QJsonObject payload{ - {"model", Settings::generalSettings().qrModel()}, {"stream", true}}; + QString sessionError; + 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(); - bool enableThinking = Settings::quickRefactorSettings().useThinking(); - provider->prepareRequest( - payload, - promptTemplate, - context, - PluginLLMCore::RequestType::QuickRefactoring, - enableTools, - enableThinking); + const bool enableTools = Settings::quickRefactorSettings().useTools(); + if (enableTools) { + Tools::registerQodeAssistTools(client->tools()); + client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations()); + } - provider->client()->setMaxToolContinuations( - Settings::toolsSettings().maxToolContinuations()); + session->systemPrompt()->setLayer( + QStringLiteral("refactor"), buildSystemPrompt(editor, range)); provider->client()->setTransferTimeout( static_cast(Settings::generalSettings().requestTimeout() * 1000)); @@ -150,43 +183,38 @@ void QuickRefactorHandler::prepareAndSendRequest( m_isRefactoringInProgress = true; connect( - provider->client(), - &::LLMQore::BaseClient::requestCompleted, - this, - &QuickRefactorHandler::handleFullResponse, - Qt::UniqueConnection); - + client, &::LLMQore::BaseClient::requestCompleted, + this, &QuickRefactorHandler::handleFullResponse, Qt::UniqueConnection); connect( - provider->client(), - &::LLMQore::BaseClient::requestFinalized, - this, - &QuickRefactorHandler::handleRequestFinalized, - Qt::UniqueConnection); - + client, &::LLMQore::BaseClient::requestFinalized, + this, &QuickRefactorHandler::handleRequestFinalized, Qt::UniqueConnection); connect( - provider->client(), - &::LLMQore::BaseClient::requestFailed, - this, - &QuickRefactorHandler::handleRequestFailed, - Qt::UniqueConnection); + client, &::LLMQore::BaseClient::requestFailed, + this, &QuickRefactorHandler::handleRequestFailed, Qt::UniqueConnection); + + std::vector> blocks; + const QString userMessage = instructions.isEmpty() + ? QStringLiteral("Refactor the code to improve its quality and maintainability.") + : instructions; + blocks.push_back(std::make_unique(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; - QJsonObject request{{"id", requestId}}; - - m_activeRequests[requestId] = {request, provider}; + m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session}; } -PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( - TextEditor::TextEditorWidget *editor, - const Utils::Text::Range &range, - const QString &instructions) +QString QuickRefactorHandler::buildSystemPrompt( + TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range) { - PluginLLMCore::ContextData context; + Q_UNUSED(range) auto textDocument = editor->textDocument(); Context::DocumentReaderQtCreator documentReader; @@ -194,7 +222,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( if (!documentInfo.document) { LOG_MESSAGE("Error: Document is not available"); - return context; + return Settings::quickRefactorSettings().systemPrompt(); } QTextCursor cursor = editor->textCursor(); @@ -270,17 +298,6 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( 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 += "\nLanguage: " + documentInfo.mimeType; systemPrompt += "\nFile path: " + documentInfo.filePath; @@ -294,7 +311,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( "\n- Your output will completely replace the selected code" : "\n- Generate ONLY the code that should be INSERTED at the position" "\n- Your output will be inserted at the cursor location"; - + systemPrompt += "\n\n## Formatting Rules:" "\n- Output ONLY the code itself, without ANY explanations or descriptions" "\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 send in answer or and other tags" "\n- The output must be ready to insert directly into the editor as-is"; - + systemPrompt += "\n\n## Indentation and Whitespace:"; - + if (cursor.hasSelection()) { QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart()); int leadingSpaces = 0; @@ -336,7 +353,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( .arg(leadingSpaces); } } - + systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code" "\n- Maintain consistent indentation for nested blocks" "\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}); } - context.systemPrompt = systemPrompt; - - QVector messages; - messages.append( - {"user", - instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability." - : instructions}); - context.history = messages; - - return context; + return systemPrompt; } void QuickRefactorHandler::handleLLMResponse( @@ -398,10 +406,14 @@ void QuickRefactorHandler::cancelRequest() auto it = m_activeRequests.find(id); if (it != m_activeRequests.end()) { - auto provider = it.value().provider; + Session *session = it.value().session; m_activeRequests.erase(it); - if (provider) - provider->cancelRequest(id); + if (session) { + if (auto *client = session->client()) + disconnect(client, nullptr, this, nullptr); + if (m_sessionManager) + m_sessionManager->removeSession(session); + } } RefactorResult result; @@ -412,11 +424,19 @@ void QuickRefactorHandler::cancelRequest() void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText) { - if (requestId == m_lastRequestId) { - m_activeRequests.remove(requestId); - QJsonObject request{{"id", requestId}}; - handleLLMResponse(fullText, request, true); - } + if (requestId != m_lastRequestId) + return; + + 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( @@ -437,15 +457,23 @@ void QuickRefactorHandler::handleRequestFinalized( void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error) { - if (requestId == m_lastRequestId) { - m_activeRequests.remove(requestId); - m_isRefactoringInProgress = false; - RefactorResult result; - result.success = false; - result.errorMessage = error; - result.editor = m_currentEditor; - emit refactoringCompleted(result); - } + if (requestId != m_lastRequestId) + return; + + 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); + + 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 diff --git a/QuickRefactorHandler.hpp b/QuickRefactorHandler.hpp index 8d58b73..2ca22eb 100644 --- a/QuickRefactorHandler.hpp +++ b/QuickRefactorHandler.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -13,11 +14,13 @@ #include #include -#include -#include namespace QodeAssist { +class SessionManager; +class Session; +class AgentFactory; + struct RefactorResult { QString newText; @@ -35,6 +38,9 @@ public: explicit QuickRefactorHandler(QObject *parent = nullptr); ~QuickRefactorHandler() override; + void setSessionManager(SessionManager *sessionManager); + void setAgentFactory(AgentFactory *agentFactory); + void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions); void cancelRequest(); @@ -56,17 +62,18 @@ private: const Utils::Text::Range &range); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); - PluginLLMCore::ContextData prepareContext( - TextEditor::TextEditorWidget *editor, - const Utils::Text::Range &range, - const QString &instructions); + QString buildSystemPrompt( + TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range); + QString pickRefactorAgent(const QString &filePath) const; struct RequestContext { QJsonObject originalRequest; - PluginLLMCore::Provider *provider; + QPointer session; }; + QPointer m_sessionManager; + QPointer m_agentFactory; QHash m_activeRequests; TextEditor::TextEditorWidget *m_currentEditor; Utils::Text::Range m_currentRange; diff --git a/context/ContextManager.cpp b/context/ContextManager.cpp index 8ac9714..3e1d36c 100644 --- a/context/ContextManager.cpp +++ b/context/ContextManager.cpp @@ -100,13 +100,9 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const { - const auto &generalSettings = Settings::generalSettings(); - - Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo); - Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString( - generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language())); - - return generalSettings.specifyPreset1() && documentLanguage == preset1Language; + Q_UNUSED(documentInfo) + // Language-specific completion presets were replaced by agent match rules. + return false; } QList> ContextManager::openedFiles(const QStringList excludeFiles) const diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..2dcd575 --- /dev/null +++ b/docs/architecture.md @@ -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. +``` diff --git a/pluginllmcore/CMakeLists.txt b/pluginllmcore/CMakeLists.txt index 55a85d8..e2b3c61 100644 --- a/pluginllmcore/CMakeLists.txt +++ b/pluginllmcore/CMakeLists.txt @@ -10,7 +10,6 @@ add_library(PluginLLMCore STATIC PromptTemplate.hpp PromptTemplateManager.hpp PromptTemplateManager.cpp ProviderID.hpp - RulesLoader.hpp RulesLoader.cpp ResponseCleaner.hpp ) diff --git a/pluginllmcore/RulesLoader.cpp b/pluginllmcore/RulesLoader.cpp deleted file mode 100644 index 4947cf7..0000000 --- a/pluginllmcore/RulesLoader.cpp +++ /dev/null @@ -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 -#include - -#include -#include -#include - -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 RulesLoader::getRuleFiles(const QString &projectPath, RulesContext context) -{ - if (projectPath.isEmpty()) { - return QVector(); - } - - QVector 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 RulesLoader::getRuleFilesForProject( - ProjectExplorer::Project *project, RulesContext context) -{ - if (!project) { - return QVector(); - } - - 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 RulesLoader::collectMarkdownFiles( - const QString &dirPath, const QString &category) -{ - QVector 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 diff --git a/pluginllmcore/RulesLoader.hpp b/pluginllmcore/RulesLoader.hpp deleted file mode 100644 index 9b17323..0000000 --- a/pluginllmcore/RulesLoader.hpp +++ /dev/null @@ -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 - -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 getRuleFiles(const QString &projectPath, RulesContext context); - static QVector getRuleFilesForProject(ProjectExplorer::Project *project, RulesContext context); - static QString loadRuleFileContent(const QString &filePath); - -private: - static QString loadAllMarkdownFiles(const QString &dirPath); - static QVector collectMarkdownFiles(const QString &dirPath, const QString &category); - static QString getProjectPath(ProjectExplorer::Project *project); -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp index 4c1bedf..6cf87b0 100644 --- a/providers/ClaudeProvider.cpp +++ b/providers/ClaudeProvider.cpp @@ -94,10 +94,7 @@ void ClaudeProvider::prepareRequest( request["temperature"] = 1.0; }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - request["temperature"] = Settings::codeCompletionSettings().temperature(); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applyModelParams(qrSettings); diff --git a/providers/DeepSeekProvider.cpp b/providers/DeepSeekProvider.cpp index ccf9dca..1bda509 100644 --- a/providers/DeepSeekProvider.cpp +++ b/providers/DeepSeekProvider.cpp @@ -86,9 +86,7 @@ void DeepSeekProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); diff --git a/providers/GoogleAIProvider.cpp b/providers/GoogleAIProvider.cpp index b47cfa0..d2baad9 100644 --- a/providers/GoogleAIProvider.cpp +++ b/providers/GoogleAIProvider.cpp @@ -92,9 +92,7 @@ void GoogleAIProvider::prepareRequest( request["generationConfig"] = generationConfig; }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); if (isThinkingEnabled) { diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index a93a77c..912ef18 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -90,9 +90,7 @@ void LMStudioProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); diff --git a/providers/LMStudioResponsesProvider.cpp b/providers/LMStudioResponsesProvider.cpp index 7a5f176..0569f04 100644 --- a/providers/LMStudioResponsesProvider.cpp +++ b/providers/LMStudioResponsesProvider.cpp @@ -82,9 +82,7 @@ void LMStudioResponsesProvider::prepareRequest( request["include"] = include; }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applyModelParams(qrSettings); diff --git a/providers/LlamaCppProvider.cpp b/providers/LlamaCppProvider.cpp index 301bb32..c017867 100644 --- a/providers/LlamaCppProvider.cpp +++ b/providers/LlamaCppProvider.cpp @@ -75,9 +75,7 @@ void LlamaCppProvider::prepareRequest( request["chat_template_kwargs"] = chatTemplateKwargs; }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); if (isThinkingEnabled) { applyThinkingMode(); diff --git a/providers/MistralAIProvider.cpp b/providers/MistralAIProvider.cpp index d166713..69314d2 100644 --- a/providers/MistralAIProvider.cpp +++ b/providers/MistralAIProvider.cpp @@ -88,9 +88,7 @@ void MistralAIProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); diff --git a/providers/OllamaCompatProvider.cpp b/providers/OllamaCompatProvider.cpp index c960d3d..5ed3678 100644 --- a/providers/OllamaCompatProvider.cpp +++ b/providers/OllamaCompatProvider.cpp @@ -70,9 +70,7 @@ void OllamaCompatProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index f3658cc..9c9d6c4 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -82,9 +82,7 @@ void OllamaProvider::prepareRequest( request["options"] = options; }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applySettings(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applySettings(qrSettings); diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index 6f9fe62..0b8ba48 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -69,9 +69,7 @@ void OpenAICompatProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); diff --git a/providers/OpenAIProvider.cpp b/providers/OpenAIProvider.cpp index 322f666..da4371b 100644 --- a/providers/OpenAIProvider.cpp +++ b/providers/OpenAIProvider.cpp @@ -88,9 +88,7 @@ void OpenAIProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); diff --git a/providers/OpenAIResponsesProvider.cpp b/providers/OpenAIResponsesProvider.cpp index 455b61d..82e6b5e 100644 --- a/providers/OpenAIResponsesProvider.cpp +++ b/providers/OpenAIResponsesProvider.cpp @@ -80,9 +80,7 @@ void OpenAIResponsesProvider::prepareRequest( request["include"] = include; }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applyModelParams(qrSettings); diff --git a/providers/QwenProvider.cpp b/providers/QwenProvider.cpp index b82b4bc..5b943b1 100644 --- a/providers/QwenProvider.cpp +++ b/providers/QwenProvider.cpp @@ -88,9 +88,7 @@ void QwenProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); diff --git a/providers/QwenResponsesProvider.cpp b/providers/QwenResponsesProvider.cpp index 1c7a31c..65d6cef 100644 --- a/providers/QwenResponsesProvider.cpp +++ b/providers/QwenResponsesProvider.cpp @@ -80,9 +80,7 @@ void QwenResponsesProvider::prepareRequest( request["include"] = include; }; - if (type == PluginLLMCore::RequestType::CodeCompletion) { - applyModelParams(Settings::codeCompletionSettings()); - } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { + if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applyModelParams(qrSettings); diff --git a/qodeassist.cpp b/qodeassist.cpp index 0a6795f..d395beb 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -34,7 +34,6 @@ #include #include -#include "ConfigurationManager.hpp" #include "QodeAssistClient.hpp" #include "UpdateStatusWidget.hpp" #include "Version.hpp" @@ -43,7 +42,6 @@ #include "chat/ChatOutputPane.h" #include "chat/NavigationPanel.hpp" #include "context/DocumentReaderQtCreator.hpp" -#include "pluginllmcore/PromptProviderFim.hpp" #include "pluginllmcore/ProvidersManager.hpp" #include "logger/RequestPerformanceLogger.hpp" #include "mcp/McpClientsManager.hpp" @@ -56,7 +54,6 @@ #include "settings/ProjectSettingsPanel.hpp" #include "settings/AgentsSettingsPage.hpp" #include "settings/ProvidersSettingsPage.hpp" -#include "sources/settings/AgentPipelinesPage.hpp" #include "settings/QuickRefactorSettings.hpp" #include "settings/SettingsConstants.hpp" @@ -65,6 +62,8 @@ #include "ProviderSecretsStore.hpp" #include +#include +#include #include "templates/Templates.hpp" #include "widgets/CustomInstructionsManager.hpp" #include "widgets/QuickRefactorDialog.hpp" @@ -74,6 +73,7 @@ #include #include #include +#include #include #include #include @@ -95,7 +95,6 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin public: QodeAssistPlugin() : m_updater(new PluginUpdater(this)) - , m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) {} ~QodeAssistPlugin() final @@ -203,8 +202,8 @@ public: m_engine, m_sessionFileRegistry, m_skillsManager}; Settings::setupProjectPanel(); - ConfigurationManager::instance().init(); + Providers::registerBuiltinProviders(); m_providerInstanceFactory = new Providers::ProviderInstanceFactory(this); m_providerSecretsStore = new Providers::ProviderSecretsStore(this); m_providerLauncher = new Providers::ProviderLauncher(this); @@ -216,6 +215,9 @@ public: m_providersPageNavigator); 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_agentsOptionsPage = Settings::createAgentsSettingsPage( m_agentFactory, m_agentsPageNavigator); @@ -342,10 +344,12 @@ public: m_qodeAssistClient = new QodeAssistClient(new LLMClientInterface( Settings::generalSettings(), Settings::codeCompletionSettings(), - PluginLLMCore::ProvidersManager::instance(), - &m_promptProvider, + *m_sessionManager, + *m_agentFactory, m_documentReader, m_performanceLogger)); + m_qodeAssistClient->setSessionManager(m_sessionManager); + m_qodeAssistClient->setAgentFactory(m_agentFactory); } bool delayedInitialize() final @@ -503,7 +507,6 @@ private: } QPointer m_qodeAssistClient; - PluginLLMCore::PromptProviderFim m_promptProvider; Context::DocumentReaderQtCreator m_documentReader; RequestPerformanceLogger m_performanceLogger; QPointer m_chatOutputPane; @@ -524,6 +527,7 @@ private: QPointer m_providersPageNavigator; std::unique_ptr m_providersOptionsPage; QPointer m_agentFactory; + QPointer m_sessionManager; QPointer m_agentsPageNavigator; std::unique_ptr m_agentsOptionsPage; QPointer m_agentPipelinesPageNavigator; diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index e311bc4..624276e 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -1,6 +1,5 @@ add_library(QodeAssistSettings STATIC GeneralSettings.hpp GeneralSettings.cpp - ConfigurationManager.hpp ConfigurationManager.cpp SettingsUtils.hpp SettingsConstants.hpp ButtonAspect.hpp @@ -47,5 +46,9 @@ target_link_libraries(QodeAssistSettings ProvidersConfig Agents 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}) diff --git a/settings/CodeCompletionSettings.cpp b/settings/CodeCompletionSettings.cpp index 4344fa8..2da67d1 100644 --- a/settings/CodeCompletionSettings.cpp +++ b/settings/CodeCompletionSettings.cpp @@ -165,54 +165,6 @@ CodeCompletionSettings::CodeCompletionSettings() "for triggering completions. This helps trigger completions based on actual code " "characters only.")); - // General Parameters Settings - temperature.setSettingsKey(Constants::CC_TEMPERATURE); - temperature.setLabelText(Tr::tr("Temperature:")); - temperature.setDefaultValue(0.2); - temperature.setRange(0.0, 2.0); - temperature.setSingleStep(0.1); - - maxTokens.setSettingsKey(Constants::CC_MAX_TOKENS); - maxTokens.setLabelText(Tr::tr("Max Tokens:")); - maxTokens.setRange(-1, 900000); - maxTokens.setDefaultValue(500); - - // Advanced Parameters - useTopP.setSettingsKey(Constants::CC_USE_TOP_P); - useTopP.setDefaultValue(false); - useTopP.setLabelText(Tr::tr("Top P:")); - - topP.setSettingsKey(Constants::CC_TOP_P); - topP.setDefaultValue(0.9); - topP.setRange(0.0, 1.0); - topP.setSingleStep(0.1); - - useTopK.setSettingsKey(Constants::CC_USE_TOP_K); - useTopK.setDefaultValue(false); - useTopK.setLabelText(Tr::tr("Top K:")); - - topK.setSettingsKey(Constants::CC_TOP_K); - topK.setDefaultValue(50); - topK.setRange(1, 1000); - - usePresencePenalty.setSettingsKey(Constants::CC_USE_PRESENCE_PENALTY); - usePresencePenalty.setDefaultValue(false); - usePresencePenalty.setLabelText(Tr::tr("Presence Penalty:")); - - presencePenalty.setSettingsKey(Constants::CC_PRESENCE_PENALTY); - presencePenalty.setDefaultValue(0.0); - presencePenalty.setRange(-2.0, 2.0); - presencePenalty.setSingleStep(0.1); - - useFrequencyPenalty.setSettingsKey(Constants::CC_USE_FREQUENCY_PENALTY); - useFrequencyPenalty.setDefaultValue(false); - useFrequencyPenalty.setLabelText(Tr::tr("Frequency Penalty:")); - - frequencyPenalty.setSettingsKey(Constants::CC_FREQUENCY_PENALTY); - frequencyPenalty.setDefaultValue(0.0); - frequencyPenalty.setRange(-2.0, 2.0); - frequencyPenalty.setSingleStep(0.1); - // Context Settings readFullFile.setSettingsKey(Constants::CC_READ_FULL_FILE); readFullFile.setLabelText(Tr::tr("Read Full File")); @@ -240,43 +192,6 @@ CodeCompletionSettings::CodeCompletionSettings() "You are an expert C++, Qt, and QML code completion assistant. Your task is to provide " "precise and contextually appropriate code completions.\n\n"); - useUserMessageTemplateForCC.setSettingsKey(Constants::CC_USE_USER_TEMPLATE); - useUserMessageTemplateForCC.setDefaultValue(true); - useUserMessageTemplateForCC.setLabelText( - Tr::tr("Use special system prompt and user message for non FIM models")); - - systemPromptForNonFimModels.setSettingsKey(Constants::CC_SYSTEM_PROMPT_FOR_NON_FIM); - systemPromptForNonFimModels.setDisplayStyle(Utils::StringAspect::TextEditDisplay); - systemPromptForNonFimModels.setLabelText(Tr::tr("System prompt for non FIM models:")); - systemPromptForNonFimModels.setDefaultValue( - "You are an expert C++, Qt, and QML code completion assistant. Your task is to provide " - "precise and contextually appropriate code completions.\n\n" - "Core Requirements:\n" - "1. Continue code exactly from the cursor position, ensuring it properly connects with any " - "existing code after the cursor\n" - "2. Never repeat existing code before or after the cursor\n" - "Specific Guidelines:\n" - "- For function calls: Complete parameters with appropriate types and names\n" - "- For class members: Respect access modifiers and class conventions\n" - "- Respect existing indentation and formatting\n" - "- Consider scope and visibility of referenced symbols\n" - "- Ensure seamless integration with code both before and after the cursor\n\n" - "Context Format:\n" - "\n" - "{{code before cursor}}{{code after cursor}}\n" - "\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" - "\n${prefix}${suffix}\n\n\n"); - customLanguages.setSettingsKey(Constants::CC_CUSTOM_LANGUAGES); customLanguages.setLabelText( Tr::tr("Additional Programming Languages for handling: Example: rust,//,rust rs,rs")); @@ -311,39 +226,6 @@ CodeCompletionSettings::CodeCompletionSettings() maxChangesCacheSize.setRange(2, 1000); maxChangesCacheSize.setDefaultValue(10); - // Ollama Settings - ollamaLivetime.setSettingsKey(Constants::CC_OLLAMA_LIVETIME); - ollamaLivetime.setToolTip( - Tr::tr("Time to suspend Ollama after completion request (in minutes), " - "Only Ollama, -1 to disable")); - ollamaLivetime.setLabelText("Livetime:"); - ollamaLivetime.setDefaultValue("5m"); - ollamaLivetime.setDisplayStyle(Utils::StringAspect::LineEditDisplay); - - contextWindow.setSettingsKey(Constants::CC_OLLAMA_CONTEXT_WINDOW); - contextWindow.setLabelText(Tr::tr("Context Window:")); - contextWindow.setRange(-1, 10000); - contextWindow.setDefaultValue(2048); - - // OpenAI Responses API Settings - openAIResponsesReasoningEffort.setSettingsKey(Constants::CC_OPENAI_RESPONSES_REASONING_EFFORT); - openAIResponsesReasoningEffort.setLabelText(Tr::tr("Reasoning effort:")); - openAIResponsesReasoningEffort.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); - openAIResponsesReasoningEffort.addOption("None"); - openAIResponsesReasoningEffort.addOption("Minimal"); - openAIResponsesReasoningEffort.addOption("Low"); - openAIResponsesReasoningEffort.addOption("Medium"); - openAIResponsesReasoningEffort.addOption("High"); - openAIResponsesReasoningEffort.setDefaultValue("Medium"); - openAIResponsesReasoningEffort.setToolTip( - Tr::tr("Constrains effort on reasoning for OpenAI gpt-5 and o-series models:\n\n" - "None: No reasoning (gpt-5.1 only)\n" - "Minimal: Minimal reasoning effort (o-series only)\n" - "Low: Low reasoning effort\n" - "Medium: Balanced reasoning (default for most models)\n" - "High: Maximum reasoning effort (gpt-5-pro only supports this)\n\n" - "Note: Reducing effort = faster responses + fewer tokens")); - resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults"); readSettings(); @@ -357,23 +239,6 @@ CodeCompletionSettings::CodeCompletionSettings() setLayouter([this]() { using namespace Layouting; - auto genGrid = Grid{}; - genGrid.addRow({Row{temperature}}); - genGrid.addRow({Row{maxTokens}}); - - auto advancedGrid = Grid{}; - advancedGrid.addRow({useTopP, topP}); - advancedGrid.addRow({useTopK, topK}); - advancedGrid.addRow({usePresencePenalty, presencePenalty}); - advancedGrid.addRow({useFrequencyPenalty, frequencyPenalty}); - - auto ollamaGrid = Grid{}; - ollamaGrid.addRow({ollamaLivetime}); - ollamaGrid.addRow({contextWindow}); - - auto openAIResponsesGrid = Grid{}; - openAIResponsesGrid.addRow({openAIResponsesReasoningEffort}); - auto contextGrid = Grid{}; contextGrid.addRow({Row{readFullFile}}); contextGrid.addRow({Row{readFileParts, readStringsBeforeCursor, readStringsAfterCursor}}); @@ -382,14 +247,7 @@ CodeCompletionSettings::CodeCompletionSettings() Row{contextGrid, Stretch{1}}, Row{useSystemPrompt, Stretch{1}}, Group{title(Tr::tr("Prompts for FIM models")), Column{systemPrompt}}, - Group{ - title(Tr::tr("Prompts for Non FIM models")), - Column{ - Row{useUserMessageTemplateForCC, Stretch{1}}, - systemPromptForNonFimModels, - userMessageTemplateForCC, - customLanguages, - }}, + customLanguages, Row{useProjectChangesCache, maxChangesCacheSize, Stretch{1}}}; auto generalSettings = Column{ @@ -418,19 +276,7 @@ CodeCompletionSettings::CodeCompletionSettings() Space{8}, Group{title(Tr::tr("Automatic Trigger Mode")), autoTriggerSettings}}}, Space{8}, - Group{title(Tr::tr("General Parameters")), - Column{ - Row{genGrid, Stretch{1}}, - }}, - Space{8}, - Group{title(Tr::tr("Advanced Parameters")), - Column{Row{advancedGrid, Stretch{1}}}}, - Space{8}, Group{title(Tr::tr("Context Settings")), contextItem}, - Space{8}, - Group{title(Tr::tr("OpenAI Responses API")), Column{Row{openAIResponsesGrid, Stretch{1}}}}, - Space{8}, - Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}}, Stretch{1}}; }); } @@ -470,16 +316,6 @@ void CodeCompletionSettings::resetSettingsToDefaults() if (reply == QMessageBox::Yes) { resetAspect(autoCompletion); resetAspect(multiLineCompletion); - resetAspect(temperature); - resetAspect(maxTokens); - resetAspect(useTopP); - resetAspect(topP); - resetAspect(useTopK); - resetAspect(topK); - resetAspect(usePresencePenalty); - resetAspect(presencePenalty); - resetAspect(useFrequencyPenalty); - resetAspect(frequencyPenalty); resetAspect(readFullFile); resetAspect(readFileParts); resetAspect(readStringsBeforeCursor); @@ -488,12 +324,6 @@ void CodeCompletionSettings::resetSettingsToDefaults() resetAspect(systemPrompt); resetAspect(useProjectChangesCache); resetAspect(maxChangesCacheSize); - resetAspect(ollamaLivetime); - resetAspect(contextWindow); - resetAspect(openAIResponsesReasoningEffort); - resetAspect(useUserMessageTemplateForCC); - resetAspect(userMessageTemplateForCC); - resetAspect(systemPromptForNonFimModels); resetAspect(customLanguages); resetAspect(showProgressWidget); resetAspect(useOpenFilesContext); @@ -527,14 +357,6 @@ void CodeCompletionSettings::migrateCompletionMode() writeSettings(); } -QString CodeCompletionSettings::processMessageToFIM(const QString &prefix, const QString &suffix) const -{ - QString result = userMessageTemplateForCC(); - result.replace("${prefix}", prefix); - result.replace("${suffix}", suffix); - return result; -} - class CodeCompletionSettingsPage : public Core::IOptionsPage { public: diff --git a/settings/CodeCompletionSettings.hpp b/settings/CodeCompletionSettings.hpp index 131aca9..8913287 100644 --- a/settings/CodeCompletionSettings.hpp +++ b/settings/CodeCompletionSettings.hpp @@ -41,23 +41,6 @@ public: Utils::BoolAspect abortAssistOnRequest{this}; Utils::BoolAspect useOpenFilesContext{this}; - // General Parameters Settings - Utils::DoubleAspect temperature{this}; - Utils::IntegerAspect maxTokens{this}; - - // Advanced Parameters - Utils::BoolAspect useTopP{this}; - Utils::DoubleAspect topP{this}; - - Utils::BoolAspect useTopK{this}; - Utils::IntegerAspect topK{this}; - - Utils::BoolAspect usePresencePenalty{this}; - Utils::DoubleAspect presencePenalty{this}; - - Utils::BoolAspect useFrequencyPenalty{this}; - Utils::DoubleAspect frequencyPenalty{this}; - // Context Settings Utils::BoolAspect readFullFile{this}; Utils::BoolAspect readFileParts{this}; @@ -65,21 +48,9 @@ public: Utils::IntegerAspect readStringsAfterCursor{this}; Utils::BoolAspect useSystemPrompt{this}; Utils::StringAspect systemPrompt{this}; - Utils::BoolAspect useUserMessageTemplateForCC{this}; - Utils::StringAspect systemPromptForNonFimModels{this}; - Utils::StringAspect userMessageTemplateForCC{this}; Utils::BoolAspect useProjectChangesCache{this}; Utils::IntegerAspect maxChangesCacheSize{this}; - // Ollama Settings - Utils::StringAspect ollamaLivetime{this}; - Utils::IntegerAspect contextWindow{this}; - - // OpenAI Responses API Settings - Utils::SelectionAspect openAIResponsesReasoningEffort{this}; - - QString processMessageToFIM(const QString &prefix, const QString &suffix) const; - private: void setupConnections(); void resetSettingsToDefaults(); diff --git a/settings/ConfigurationManager.cpp b/settings/ConfigurationManager.cpp deleted file mode 100644 index 19cb092..0000000 --- a/settings/ConfigurationManager.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include -#include -#include - -#include - -#include "Logger.hpp" -#include "ProviderNameMigration.hpp" - -namespace QodeAssist::Settings { - -ConfigurationManager::ConfigurationManager(QObject *parent) - : QObject(parent) -{} - -ConfigurationManager &ConfigurationManager::instance() -{ - static ConfigurationManager instance; - return instance; -} - -QVector ConfigurationManager::getPredefinedConfigurations( - ConfigurationType type) -{ - QVector 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 *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 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 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 &configs = configurations(type); - for (const AIConfiguration &config : configs) { - if (config.id == id) { - return config; - } - } - return AIConfiguration(); -} - -} // namespace QodeAssist::Settings - diff --git a/settings/ConfigurationManager.hpp b/settings/ConfigurationManager.hpp deleted file mode 100644 index 306ad1f..0000000 --- a/settings/ConfigurationManager.hpp +++ /dev/null @@ -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 -#include -#include - -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 configurations(ConfigurationType type) const; - AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const; - - QString getConfigurationDirectory(ConfigurationType type) const; - - static QVector 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 m_ccConfigurations; - QVector m_caConfigurations; - QVector m_qrConfigurations; -}; - -} // namespace QodeAssist::Settings - diff --git a/settings/GeneralSettings.cpp b/settings/GeneralSettings.cpp index 8f7df30..821784b 100644 --- a/settings/GeneralSettings.cpp +++ b/settings/GeneralSettings.cpp @@ -7,46 +7,31 @@ #include #include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include #include #include +#include #include -#include +#include #include -#include -#include -#include -#include -#include -#include -#include +#include #include "../Version.hpp" -#include "ConfigurationManager.hpp" +#include "AgentRosterWidget.hpp" +#include "AgentsSettingsPage.hpp" #include "Logger.hpp" -#include "ProviderNameMigration.hpp" +#include "PipelinesConfig.hpp" #include "SettingsConstants.hpp" -#include "SettingsDialog.hpp" #include "SettingsTr.hpp" #include "SettingsUtils.hpp" #include "UpdateDialog.hpp" -namespace QodeAssist::Settings { +#include -void addDialogButtons(QBoxLayout *layout, QAbstractButton *okButton, QAbstractButton *cancelButton) -{ -#if defined(Q_OS_MACOS) - layout->addWidget(cancelButton); - layout->addWidget(okButton); -#else - layout->addWidget(okButton); - layout->addWidget(cancelButton); -#endif -} +namespace QodeAssist::Settings { GeneralSettings &generalSettings() { @@ -86,264 +71,16 @@ GeneralSettings::GeneralSettings() resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS; checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE; - - ccPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); - ccPresetConfig.setLabelText(Tr::tr("Quick Setup")); - loadPresetConfigurations(ccPresetConfig, ConfigurationType::CodeCompletion); - - ccConfigureApiKey.m_buttonText = Tr::tr("Configure API Key"); - ccConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys"); - - caPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); - caPresetConfig.setLabelText(Tr::tr("Quick Setup")); - loadPresetConfigurations(caPresetConfig, ConfigurationType::Chat); - - caConfigureApiKey.m_buttonText = Tr::tr("Configure API Key"); - caConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys"); - - qrPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); - qrPresetConfig.setLabelText(Tr::tr("Quick Setup")); - loadPresetConfigurations(qrPresetConfig, ConfigurationType::QuickRefactor); - - qrConfigureApiKey.m_buttonText = Tr::tr("Configure API Key"); - qrConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys"); - - initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama (Native)"); - ccProvider.setReadOnly(true); - ccSelectProvider.m_buttonText = TrConstants::SELECT; - - initStringAspect(ccModel, Constants::CC_MODEL, TrConstants::MODEL, "qwen2.5-coder:7b"); - ccModel.setHistoryCompleter(Constants::CC_MODEL_HISTORY); - ccSelectModel.m_buttonText = TrConstants::SELECT; - - initStringAspect(ccTemplate, Constants::CC_TEMPLATE, TrConstants::TEMPLATE, "Ollama FIM"); - ccTemplate.setReadOnly(true); - ccSelectTemplate.m_buttonText = TrConstants::SELECT; - - initStringAspect(ccUrl, Constants::CC_URL, TrConstants::URL, "http://localhost:11434"); - ccUrl.setHistoryCompleter(Constants::CC_URL_HISTORY); - ccSetUrl.m_buttonText = TrConstants::SELECT; - - initStringAspect(ccCustomEndpoint, Constants::CC_CUSTOM_ENDPOINT, TrConstants::CUSTOM_ENDPOINT, ""); - ccCustomEndpoint.setHistoryCompleter(Constants::CC_CUSTOM_ENDPOINT_HISTORY); - - ccStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay); - ccStatus.setLabelText(TrConstants::STATUS); - ccStatus.setDefaultValue(""); - ccTest.m_buttonText = TrConstants::TEST; - - ccTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay); - ccTemplateDescription.setReadOnly(true); - ccTemplateDescription.setDefaultValue(""); - - ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; - ccLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG; - ccLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)"); - ccOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER; - ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); - ccOpenConfigFolder.m_isCompact = true; - - // preset1 - specifyPreset1.setSettingsKey(Constants::CC_SPECIFY_PRESET1); - specifyPreset1.setLabelText(TrConstants::ADD_NEW_PRESET); - specifyPreset1.setDefaultValue(false); - - preset1Language.setSettingsKey(Constants::CC_PRESET1_LANGUAGE); - preset1Language.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); - // see ProgrammingLanguageUtils - preset1Language.addOption("qml"); - preset1Language.addOption("c/c++"); - preset1Language.addOption("python"); - - initStringAspect( - ccPreset1Provider, - Constants::CC_PRESET1_PROVIDER, - TrConstants::PROVIDER, - "Ollama (Native)"); - ccPreset1Provider.setReadOnly(true); - ccPreset1SelectProvider.m_buttonText = TrConstants::SELECT; - - initStringAspect( - ccPreset1Url, Constants::CC_PRESET1_URL, TrConstants::URL, "http://localhost:11434"); - ccPreset1Url.setHistoryCompleter(Constants::CC_PRESET1_URL_HISTORY); - ccPreset1SetUrl.m_buttonText = TrConstants::SELECT; - - initStringAspect( - ccPreset1CustomEndpoint, - Constants::CC_PRESET1_CUSTOM_ENDPOINT, - TrConstants::CUSTOM_ENDPOINT, - ""); - ccPreset1CustomEndpoint.setHistoryCompleter(Constants::CC_PRESET1_CUSTOM_ENDPOINT_HISTORY); - - initStringAspect( - ccPreset1Model, Constants::CC_PRESET1_MODEL, TrConstants::MODEL, "qwen2.5-coder:7b"); - ccPreset1Model.setHistoryCompleter(Constants::CC_PRESET1_MODEL_HISTORY); - ccPreset1SelectModel.m_buttonText = TrConstants::SELECT; - - initStringAspect( - ccPreset1Template, Constants::CC_PRESET1_TEMPLATE, TrConstants::TEMPLATE, "Ollama FIM"); - ccPreset1Template.setReadOnly(true); - ccPreset1SelectTemplate.m_buttonText = TrConstants::SELECT; - - // chat assistance - initStringAspect(caProvider, Constants::CA_PROVIDER, TrConstants::PROVIDER, "Ollama (Native)"); - caProvider.setReadOnly(true); - caSelectProvider.m_buttonText = TrConstants::SELECT; - - initStringAspect(caModel, Constants::CA_MODEL, TrConstants::MODEL, "qwen3.5:9b"); - caModel.setHistoryCompleter(Constants::CA_MODEL_HISTORY); - caSelectModel.m_buttonText = TrConstants::SELECT; - - initStringAspect(caTemplate, Constants::CA_TEMPLATE, TrConstants::TEMPLATE, "Ollama Chat"); - caTemplate.setReadOnly(true); - - caSelectTemplate.m_buttonText = TrConstants::SELECT; - - initStringAspect(caUrl, Constants::CA_URL, TrConstants::URL, "http://localhost:11434"); - caUrl.setHistoryCompleter(Constants::CA_URL_HISTORY); - caSetUrl.m_buttonText = TrConstants::SELECT; - - initStringAspect(caCustomEndpoint, Constants::CA_CUSTOM_ENDPOINT, TrConstants::CUSTOM_ENDPOINT, ""); - caCustomEndpoint.setHistoryCompleter(Constants::CA_CUSTOM_ENDPOINT_HISTORY); - - caStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay); - caStatus.setLabelText(TrConstants::STATUS); - caStatus.setDefaultValue(""); - caTest.m_buttonText = TrConstants::TEST; - - caTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay); - caTemplateDescription.setReadOnly(true); - caTemplateDescription.setDefaultValue(""); - - caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; - caLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG; - caLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)"); - caOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER; - caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); - caOpenConfigFolder.m_isCompact = true; - - // quick refactor settings - initStringAspect(qrProvider, Constants::QR_PROVIDER, TrConstants::PROVIDER, "Ollama (Native)"); - qrProvider.setReadOnly(true); - qrSelectProvider.m_buttonText = TrConstants::SELECT; - - initStringAspect(qrModel, Constants::QR_MODEL, TrConstants::MODEL, "qwen3.5:9b"); - qrModel.setHistoryCompleter(Constants::QR_MODEL_HISTORY); - qrSelectModel.m_buttonText = TrConstants::SELECT; - - initStringAspect(qrTemplate, Constants::QR_TEMPLATE, TrConstants::TEMPLATE, "Ollama Chat"); - qrTemplate.setReadOnly(true); - - qrSelectTemplate.m_buttonText = TrConstants::SELECT; - - initStringAspect(qrUrl, Constants::QR_URL, TrConstants::URL, "http://localhost:11434"); - qrUrl.setHistoryCompleter(Constants::QR_URL_HISTORY); - qrSetUrl.m_buttonText = TrConstants::SELECT; - - initStringAspect(qrCustomEndpoint, Constants::QR_CUSTOM_ENDPOINT, TrConstants::CUSTOM_ENDPOINT, ""); - qrCustomEndpoint.setHistoryCompleter(Constants::QR_CUSTOM_ENDPOINT_HISTORY); - - qrStatus.setDisplayStyle(Utils::StringAspect::LabelDisplay); - qrStatus.setLabelText(TrConstants::STATUS); - qrStatus.setDefaultValue(""); - qrTest.m_buttonText = TrConstants::TEST; - - qrTemplateDescription.setDisplayStyle(Utils::StringAspect::TextEditDisplay); - qrTemplateDescription.setReadOnly(true); - qrTemplateDescription.setDefaultValue(""); - - qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; - qrLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG; - qrLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)"); - qrOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER; - qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); - qrOpenConfigFolder.m_isCompact = true; - - ccShowTemplateInfo.m_icon = Utils::Icons::INFO.icon(); - ccShowTemplateInfo.m_tooltip = Tr::tr("Show template information"); - ccShowTemplateInfo.m_isCompact = true; - - caShowTemplateInfo.m_icon = Utils::Icons::INFO.icon(); - caShowTemplateInfo.m_tooltip = Tr::tr("Show template information"); - caShowTemplateInfo.m_isCompact = true; - - qrShowTemplateInfo.m_icon = Utils::Icons::INFO.icon(); - qrShowTemplateInfo.m_tooltip = Tr::tr("Show template information"); - qrShowTemplateInfo.m_isCompact = true; readSettings(); - auto migrateProviderAspect = [](Utils::StringAspect &aspect) { - const QString migrated = migrateProviderName(aspect.value()); - if (migrated != aspect.value()) - aspect.setValue(migrated); - }; - migrateProviderAspect(ccProvider); - migrateProviderAspect(ccPreset1Provider); - migrateProviderAspect(caProvider); - migrateProviderAspect(qrProvider); - writeSettings(); - Logger::instance().setLoggingEnabled(enableLogging()); setupConnections(); - updatePreset1Visiblity(specifyPreset1.value()); - setLayouter([this]() { using namespace Layouting; - auto ccGrid = Grid{}; - ccGrid.addRow({ccProvider, ccSelectProvider}); - ccGrid.addRow({ccUrl, ccSetUrl}); - ccGrid.addRow({ccCustomEndpoint}); - ccGrid.addRow({ccModel, ccSelectModel}); - ccGrid.addRow({ccTemplate, ccSelectTemplate, ccShowTemplateInfo}); - - auto ccPreset1Grid = Grid{}; - ccPreset1Grid.addRow({ccPreset1Provider, ccPreset1SelectProvider}); - ccPreset1Grid.addRow({ccPreset1Url, ccPreset1SetUrl}); - ccPreset1Grid.addRow({ccPreset1CustomEndpoint}); - ccPreset1Grid.addRow({ccPreset1Model, ccPreset1SelectModel}); - ccPreset1Grid.addRow({ccPreset1Template, ccPreset1SelectTemplate}); - - auto caGrid = Grid{}; - caGrid.addRow({caProvider, caSelectProvider}); - caGrid.addRow({caUrl, caSetUrl}); - caGrid.addRow({caCustomEndpoint}); - caGrid.addRow({caModel, caSelectModel}); - caGrid.addRow({caTemplate, caSelectTemplate, caShowTemplateInfo}); - - auto qrGrid = Grid{}; - qrGrid.addRow({qrProvider, qrSelectProvider}); - qrGrid.addRow({qrUrl, qrSetUrl}); - qrGrid.addRow({qrCustomEndpoint}); - qrGrid.addRow({qrModel, qrSelectModel}); - qrGrid.addRow({qrTemplate, qrSelectTemplate, qrShowTemplateInfo}); - - auto ccGroup = Group{ - title(TrConstants::CODE_COMPLETION), - Column{ - Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}}, - Row{ccPresetConfig, ccConfigureApiKey, Stretch{1}}, - ccGrid, - Row{specifyPreset1, preset1Language, Stretch{1}}, - ccPreset1Grid}}; - - auto caGroup = Group{ - title(TrConstants::CHAT_ASSISTANT), - Column{ - Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}}, - Row{caPresetConfig, caConfigureApiKey, Stretch{1}}, - caGrid}}; - - auto qrGroup = Group{ - title(TrConstants::QUICK_REFACTOR), - Column{ - Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}}, - Row{qrPresetConfig, qrConfigureApiKey, Stretch{1}}, - qrGrid}}; - auto networkGroup = Group{ title(Tr::tr("Network")), Column{Row{requestTimeout, Stretch{1}}}}; @@ -362,233 +99,18 @@ GeneralSettings::GeneralSettings() supportLinks->setOpenExternalLinks(true); supportLinks->setTextFormat(Qt::RichText); - auto rootLayout = Column{ - Row{supportLabel, supportLinks, Stretch{1}, checkUpdate, resetToDefaults}, + return Column{ + Row{supportLabel, supportLinks, Stretch{1}}, Space{8}, Row{enableQodeAssist, Stretch{1}}, Row{enableLogging, Stretch{1}}, Row{enableCheckUpdate, Stretch{1}}, Space{8}, networkGroup, - Space{8}, - ccGroup, - Space{8}, - caGroup, - Space{8}, - qrGroup, Stretch{1}}; - - return rootLayout; }); } -void GeneralSettings::showSelectionDialog( - const QStringList &data, Utils::StringAspect &aspect, const QString &title, const QString &text) -{ - if (data.isEmpty()) - return; - - bool ok; - QInputDialog dialog(Core::ICore::dialogParent()); - dialog.setWindowTitle(title); - dialog.setLabelText(text); - dialog.setComboBoxItems(data); - dialog.setComboBoxEditable(false); - dialog.setFixedSize(400, 150); - - if (dialog.exec() == QDialog::Accepted) { - QString result = dialog.textValue(); - if (!result.isEmpty()) { - aspect.setValue(result); - writeSettings(); - } - } -} - -void GeneralSettings::showModelsNotFoundDialog(Utils::StringAspect &aspect) -{ - SettingsDialog dialog(TrConstants::CONNECTION_ERROR); - dialog.addLabel(TrConstants::NO_MODELS_FOUND); - dialog.addLabel(TrConstants::CHECK_CONNECTION); - dialog.addSpacing(); - - ButtonAspect *providerButton = nullptr; - ButtonAspect *urlButton = nullptr; - - if (&aspect == &ccModel) { - providerButton = &ccSelectProvider; - urlButton = &ccSetUrl; - } else if (&aspect == &caModel) { - providerButton = &caSelectProvider; - urlButton = &caSetUrl; - } else if (&aspect == &qrModel) { - providerButton = &qrSelectProvider; - urlButton = &qrSetUrl; - } - - if (providerButton && urlButton) { - auto selectProviderBtn = new QPushButton(TrConstants::SELECT_PROVIDER); - auto selectUrlBtn = new QPushButton(TrConstants::SELECT_URL); - auto enterManuallyBtn = new QPushButton(TrConstants::ENTER_MODEL_MANUALLY); - auto configureApiKeyBtn = new QPushButton(TrConstants::CONFIGURE_API_KEY); - - connect(selectProviderBtn, &QPushButton::clicked, &dialog, [this, providerButton, &dialog]() { - dialog.close(); - emit providerButton->clicked(); - }); - - connect(selectUrlBtn, &QPushButton::clicked, &dialog, [this, urlButton, &dialog]() { - dialog.close(); - emit urlButton->clicked(); - }); - - connect(enterManuallyBtn, &QPushButton::clicked, &dialog, [this, &aspect, &dialog]() { - dialog.close(); - showModelsNotSupportedDialog(aspect); - }); - - connect(configureApiKeyBtn, &QPushButton::clicked, &dialog, [&dialog]() { - dialog.close(); - Settings::showSettings(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID); - }); - - dialog.buttonLayout()->addWidget(selectProviderBtn); - dialog.buttonLayout()->addWidget(selectUrlBtn); - dialog.buttonLayout()->addWidget(enterManuallyBtn); - dialog.buttonLayout()->addWidget(configureApiKeyBtn); - } - - auto closeBtn = new QPushButton(TrConstants::CLOSE); - connect(closeBtn, &QPushButton::clicked, &dialog, &QDialog::close); - dialog.buttonLayout()->addWidget(closeBtn); - - dialog.exec(); -} - -void GeneralSettings::showModelsNotSupportedDialog(Utils::StringAspect &aspect) -{ - SettingsDialog dialog(TrConstants::MODEL_SELECTION); - dialog.addLabel(TrConstants::MODEL_LISTING_NOT_SUPPORTED_INFO); - dialog.addSpacing(); - - QString key = QString("CompleterHistory/") - .append( - (&aspect == &ccModel) ? Constants::CC_MODEL_HISTORY - : (&aspect == &caModel) ? Constants::CA_MODEL_HISTORY - : Constants::QR_MODEL_HISTORY); -#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0) - QStringList historyList - = Utils::QtcSettings().value(Utils::Key(key.toLocal8Bit())).toStringList(); -#else - QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList(); -#endif - - auto modelList = dialog.addComboBox(historyList, aspect.value()); - dialog.addSpacing(); - - auto okButton = new QPushButton(TrConstants::OK); - connect(okButton, &QPushButton::clicked, &dialog, [this, &aspect, modelList, &dialog]() { - QString value = modelList->currentText().trimmed(); - if (!value.isEmpty()) { - aspect.setValue(value); - writeSettings(); - dialog.accept(); - } - }); - - auto cancelButton = new QPushButton(TrConstants::CANCEL); - connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject); - - addDialogButtons(dialog.buttonLayout(), okButton, cancelButton); - - modelList->setFocus(); - dialog.exec(); -} - -void GeneralSettings::showUrlSelectionDialog( - Utils::StringAspect &aspect, const QStringList &predefinedUrls) -{ - SettingsDialog dialog(TrConstants::URL_SELECTION); - dialog.addLabel(TrConstants::URL_SELECTION_INFO); - dialog.addSpacing(); - - QStringList allUrls = predefinedUrls; - QString key = QString("CompleterHistory/") - .append( - (&aspect == &ccUrl) ? Constants::CC_URL_HISTORY - : (&aspect == &ccPreset1Url) ? Constants::CC_PRESET1_URL_HISTORY - : (&aspect == &caUrl) ? Constants::CA_URL_HISTORY - : Constants::QR_URL_HISTORY); -#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0) - QStringList historyList - = Utils::QtcSettings().value(Utils::Key(key.toLocal8Bit())).toStringList(); -#else - QStringList historyList = qtcSettings()->value(Utils::Key(key.toLocal8Bit())).toStringList(); -#endif - allUrls.append(historyList); - allUrls.removeDuplicates(); - - auto urlList = dialog.addComboBox(allUrls, aspect.value()); - dialog.addSpacing(); - - auto okButton = new QPushButton(TrConstants::OK); - connect(okButton, &QPushButton::clicked, &dialog, [this, &aspect, urlList, &dialog]() { - QString value = urlList->currentText().trimmed(); - if (!value.isEmpty()) { - aspect.setValue(value); - writeSettings(); - dialog.accept(); - } - }); - - auto cancelButton = new QPushButton(TrConstants::CANCEL); - connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject); - - addDialogButtons(dialog.buttonLayout(), okButton, cancelButton); - - urlList->setFocus(); - dialog.exec(); -} - -void GeneralSettings::showTemplateInfoDialog( - const Utils::StringAspect &descriptionAspect, const QString &templateName) -{ - SettingsDialog dialog(Tr::tr("Template Information")); - dialog.addLabel(QString("%1: %2").arg(Tr::tr("Template"), templateName)); - dialog.addSpacing(); - - auto *descriptionLabel = new QLabel(Tr::tr("Description:")); - dialog.layout()->addWidget(descriptionLabel); - - auto *textEdit = new QTextEdit(); - textEdit->setReadOnly(true); - textEdit->setMinimumHeight(200); - textEdit->setMinimumWidth(500); - textEdit->setText(descriptionAspect.value()); - dialog.layout()->addWidget(textEdit); - - dialog.addSpacing(); - - auto *closeButton = new QPushButton(TrConstants::CLOSE); - connect(closeButton, &QPushButton::clicked, &dialog, &QDialog::accept); - dialog.buttonLayout()->addWidget(closeButton); - - dialog.exec(); -} - -void GeneralSettings::updatePreset1Visiblity(bool state) -{ - ccPreset1Provider.setVisible(specifyPreset1.volatileValue()); - ccPreset1SelectProvider.updateVisibility(specifyPreset1.volatileValue()); - ccPreset1Url.setVisible(specifyPreset1.volatileValue()); - ccPreset1SetUrl.updateVisibility(specifyPreset1.volatileValue()); - ccPreset1Model.setVisible(specifyPreset1.volatileValue()); - ccPreset1SelectModel.updateVisibility(specifyPreset1.volatileValue()); - ccPreset1Template.setVisible(specifyPreset1.volatileValue()); - ccPreset1SelectTemplate.updateVisibility(specifyPreset1.volatileValue()); - ccPreset1CustomEndpoint.setVisible(specifyPreset1.volatileValue()); -} - void GeneralSettings::setupConnections() { connect(&enableLogging, &Utils::BoolAspect::volatileValueChanged, this, [this]() { @@ -598,96 +120,11 @@ void GeneralSettings::setupConnections() connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() { QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent()); }); - - connect(&ccPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() { - applyPresetConfiguration(ccPresetConfig.volatileValue(), ConfigurationType::CodeCompletion); - ccPresetConfig.setValue(0); - }); - - connect(&ccConfigureApiKey, &ButtonAspect::clicked, this, []() { - Settings::showSettings(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID); - }); - - connect(&caPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() { - applyPresetConfiguration(caPresetConfig.volatileValue(), ConfigurationType::Chat); - caPresetConfig.setValue(0); - }); - - connect(&caConfigureApiKey, &ButtonAspect::clicked, this, []() { - Settings::showSettings(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID); - }); - - connect(&qrPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() { - applyPresetConfiguration(qrPresetConfig.volatileValue(), ConfigurationType::QuickRefactor); - qrPresetConfig.setValue(0); - }); - - connect(&qrConfigureApiKey, &ButtonAspect::clicked, this, []() { - Settings::showSettings(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID); - }); - - connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() { - updatePreset1Visiblity(specifyPreset1.volatileValue()); - }); - connect(&ccShowTemplateInfo, &ButtonAspect::clicked, this, [this]() { - showTemplateInfoDialog(ccTemplateDescription, ccTemplate.value()); - }); - - connect(&caShowTemplateInfo, &ButtonAspect::clicked, this, [this]() { - showTemplateInfoDialog(caTemplateDescription, caTemplate.value()); - }); - - connect(&qrShowTemplateInfo, &ButtonAspect::clicked, this, [this]() { - showTemplateInfoDialog(qrTemplateDescription, qrTemplate.value()); - }); - - connect(&ccSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("cc"); }); - connect(&ccLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("cc"); }); - - connect(&caSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("ca"); }); - connect(&caLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("ca"); }); - - connect(&qrSaveConfig, &ButtonAspect::clicked, this, [this]() { onSaveConfiguration("qr"); }); - connect(&qrLoadConfig, &ButtonAspect::clicked, this, [this]() { onLoadConfiguration("qr"); }); - - connect(&ccOpenConfigFolder, &ButtonAspect::clicked, this, [this]() { - auto &manager = ConfigurationManager::instance(); - QString path = manager.getConfigurationDirectory(ConfigurationType::CodeCompletion); - QDir dir(path); - if (!dir.exists()) { - dir.mkpath("."); - } - QUrl url = QUrl::fromLocalFile(dir.absolutePath()); - QDesktopServices::openUrl(url); - }); - - connect(&caOpenConfigFolder, &ButtonAspect::clicked, this, [this]() { - auto &manager = ConfigurationManager::instance(); - QString path = manager.getConfigurationDirectory(ConfigurationType::Chat); - QDir dir(path); - if (!dir.exists()) { - dir.mkpath("."); - } - QUrl url = QUrl::fromLocalFile(dir.absolutePath()); - QDesktopServices::openUrl(url); - }); - - connect(&qrOpenConfigFolder, &ButtonAspect::clicked, this, [this]() { - auto &manager = ConfigurationManager::instance(); - QString path = manager.getConfigurationDirectory(ConfigurationType::QuickRefactor); - QDir dir(path); - if (!dir.exists()) { - dir.mkpath("."); - } - QUrl url = QUrl::fromLocalFile(dir.absolutePath()); - QDesktopServices::openUrl(url); - }); } void GeneralSettings::resetPageToDefaults() { - QMessageBox::StandardButton reply; - reply = QMessageBox::question( + const QMessageBox::StandardButton reply = QMessageBox::question( Core::ICore::dialogParent(), TrConstants::RESET_SETTINGS, TrConstants::CONFIRMATION, @@ -697,304 +134,11 @@ void GeneralSettings::resetPageToDefaults() resetAspect(enableQodeAssist); resetAspect(enableLogging); resetAspect(requestTimeout); - resetAspect(ccProvider); - resetAspect(ccModel); - resetAspect(ccTemplate); - resetAspect(ccUrl); - resetAspect(caProvider); - resetAspect(caModel); - resetAspect(caTemplate); - resetAspect(caUrl); resetAspect(enableCheckUpdate); - resetAspect(specifyPreset1); - resetAspect(preset1Language); - resetAspect(ccPreset1Provider); - resetAspect(ccPreset1Model); - resetAspect(ccPreset1Template); - resetAspect(ccPreset1Url); - resetAspect(ccCustomEndpoint); - resetAspect(ccPreset1CustomEndpoint); - resetAspect(caCustomEndpoint); - resetAspect(qrProvider); - resetAspect(qrModel); - resetAspect(qrTemplate); - resetAspect(qrUrl); - resetAspect(qrCustomEndpoint); writeSettings(); } } -void GeneralSettings::onSaveConfiguration(const QString &prefix) -{ - bool ok; - QString configName = QInputDialog::getText( - Core::ICore::dialogParent(), - TrConstants::SAVE_CONFIGURATION, - TrConstants::CONFIGURATION_NAME, - QLineEdit::Normal, - QString(), - &ok); - - if (!ok || configName.trimmed().isEmpty()) { - return; - } - - AIConfiguration config; - config.id = QUuid::createUuid().toString(QUuid::WithoutBraces); - config.name = configName.trimmed(); - - if (prefix == "cc") { - config.provider = ccProvider.value(); - config.model = ccModel.value(); - config.templateName = ccTemplate.value(); - config.url = ccUrl.value(); - config.customEndpoint = ccCustomEndpoint.value(); - config.type = ConfigurationType::CodeCompletion; - } else if (prefix == "ca") { - config.provider = caProvider.value(); - config.model = caModel.value(); - config.templateName = caTemplate.value(); - config.url = caUrl.value(); - config.customEndpoint = caCustomEndpoint.value(); - config.type = ConfigurationType::Chat; - } else if (prefix == "qr") { - config.provider = qrProvider.value(); - config.model = qrModel.value(); - config.templateName = qrTemplate.value(); - config.url = qrUrl.value(); - config.customEndpoint = qrCustomEndpoint.value(); - config.type = ConfigurationType::QuickRefactor; - } - - auto &manager = ConfigurationManager::instance(); - if (manager.saveConfiguration(config)) { - QMessageBox::information( - Core::ICore::dialogParent(), - TrConstants::SAVE_CONFIGURATION, - TrConstants::CONFIGURATION_SAVED); - } else { - QMessageBox::warning( - Core::ICore::dialogParent(), - TrConstants::SAVE_CONFIGURATION, - Tr::tr("Failed to save configuration. Check logs for details.")); - } -} - -void GeneralSettings::onLoadConfiguration(const QString &prefix) -{ - ConfigurationType type; - if (prefix == "cc") { - type = ConfigurationType::CodeCompletion; - } else if (prefix == "ca") { - type = ConfigurationType::Chat; - } else if (prefix == "qr") { - type = ConfigurationType::QuickRefactor; - } else { - return; - } - - auto &manager = ConfigurationManager::instance(); - manager.loadConfigurations(type); - - QVector configs = manager.configurations(type); - if (configs.isEmpty()) { - QMessageBox::information( - Core::ICore::dialogParent(), - TrConstants::LOAD_CONFIGURATION, - TrConstants::NO_CONFIGURATIONS_FOUND); - return; - } - - SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION); - dialog.addLabel(TrConstants::SELECT_CONFIGURATION); - - int predefinedCount = 0; - for (const AIConfiguration &config : configs) { - if (config.isPredefined) { - predefinedCount++; - } - } - - if (predefinedCount > 0) { - auto *hintLabel = dialog.addLabel( - Tr::tr("[Preset] configurations are predefined cloud models ready to use.")); - QFont hintFont = hintLabel->font(); - hintFont.setItalic(true); - hintFont.setPointSize(hintFont.pointSize() - 1); - hintLabel->setFont(hintFont); - hintLabel->setStyleSheet("color: gray;"); - } - - dialog.addSpacing(); - - QStringList configNames; - for (const AIConfiguration &config : configs) { - QString displayName = config.name; - if (config.isPredefined) { - displayName = QString("[Preset] %1").arg(config.name); - } - configNames.append(displayName); - } - - auto configList = dialog.addComboBox(configNames, QString()); - dialog.addSpacing(); - - auto *deleteButton = new QPushButton(TrConstants::DELETE_CONFIGURATION); - auto *okButton = new QPushButton(TrConstants::OK); - auto *cancelButton = new QPushButton(TrConstants::CANCEL); - - auto updateDeleteButtonState = [&]() { - int currentIndex = configList->currentIndex(); - if (currentIndex >= 0 && currentIndex < configs.size()) { - deleteButton->setEnabled(!configs[currentIndex].isPredefined); - } - }; - - connect(configList, - QOverload::of(&QComboBox::currentIndexChanged), - updateDeleteButtonState); - - updateDeleteButtonState(); - - connect(deleteButton, &QPushButton::clicked, &dialog, [&]() { - int currentIndex = configList->currentIndex(); - if (currentIndex >= 0 && currentIndex < configs.size()) { - const AIConfiguration &configToDelete = configs[currentIndex]; - if (configToDelete.isPredefined) { - QMessageBox::information( - &dialog, - TrConstants::DELETE_CONFIGURATION, - Tr::tr("Predefined configurations cannot be deleted.")); - return; - } - - QMessageBox::StandardButton reply = QMessageBox::question( - &dialog, - TrConstants::DELETE_CONFIGURATION, - TrConstants::CONFIRM_DELETE_CONFIG, - QMessageBox::Yes | QMessageBox::No); - - if (reply == QMessageBox::Yes) { - if (manager.deleteConfiguration(configToDelete.id, type)) { - dialog.accept(); - onLoadConfiguration(prefix); - } else { - QMessageBox::warning( - &dialog, - TrConstants::DELETE_CONFIGURATION, - Tr::tr("Failed to delete configuration.")); - } - } - } - }); - - connect(okButton, &QPushButton::clicked, &dialog, [&]() { - int currentIndex = configList->currentIndex(); - if (currentIndex >= 0 && currentIndex < configs.size()) { - const AIConfiguration &config = configs[currentIndex]; - - if (prefix == "cc") { - ccProvider.setValue(config.provider); - ccModel.setValue(config.model); - ccTemplate.setValue(config.templateName); - ccUrl.setValue(config.url); - ccCustomEndpoint.setValue(config.customEndpoint); - } else if (prefix == "ca") { - caProvider.setValue(config.provider); - caModel.setValue(config.model); - caTemplate.setValue(config.templateName); - caUrl.setValue(config.url); - caCustomEndpoint.setValue(config.customEndpoint); - } else if (prefix == "qr") { - qrProvider.setValue(config.provider); - qrModel.setValue(config.model); - qrTemplate.setValue(config.templateName); - qrUrl.setValue(config.url); - qrCustomEndpoint.setValue(config.customEndpoint); - } - - writeSettings(); - QMessageBox::information( - Core::ICore::dialogParent(), - TrConstants::LOAD_CONFIGURATION, - TrConstants::CONFIGURATION_LOADED); - dialog.accept(); - } - }); - - connect(cancelButton, &QPushButton::clicked, &dialog, &QDialog::reject); - - dialog.buttonLayout()->addWidget(deleteButton); - addDialogButtons(dialog.buttonLayout(), okButton, cancelButton); - - configList->setFocus(); - dialog.exec(); -} - -void GeneralSettings::loadPresetConfigurations(Utils::SelectionAspect &aspect, - ConfigurationType type) -{ - QVector presets = ConfigurationManager::getPredefinedConfigurations(type); - - if (type == ConfigurationType::CodeCompletion) { - m_ccPresets = presets; - } else if (type == ConfigurationType::Chat) { - m_caPresets = presets; - } else if (type == ConfigurationType::QuickRefactor) { - m_qrPresets = presets; - } - - aspect.addOption(Tr::tr("-- Select Preset --")); - for (const AIConfiguration &config : presets) { - aspect.addOption(config.name); - } - aspect.setDefaultValue(0); -} - -void GeneralSettings::applyPresetConfiguration(int index, ConfigurationType type) -{ - if (index <= 0) { - return; - } - - QVector *presets = nullptr; - if (type == ConfigurationType::CodeCompletion) { - presets = &m_ccPresets; - } else if (type == ConfigurationType::Chat) { - presets = &m_caPresets; - } else if (type == ConfigurationType::QuickRefactor) { - presets = &m_qrPresets; - } - - if (!presets || index - 1 >= presets->size()) { - return; - } - - const AIConfiguration &config = presets->at(index - 1); - - if (type == ConfigurationType::CodeCompletion) { - ccProvider.setValue(config.provider); - ccModel.setValue(config.model); - ccTemplate.setValue(config.templateName); - ccUrl.setValue(config.url); - ccCustomEndpoint.setValue(config.customEndpoint); - } else if (type == ConfigurationType::Chat) { - caProvider.setValue(config.provider); - caModel.setValue(config.model); - caTemplate.setValue(config.templateName); - caUrl.setValue(config.url); - caCustomEndpoint.setValue(config.customEndpoint); - } else if (type == ConfigurationType::QuickRefactor) { - qrProvider.setValue(config.provider); - qrModel.setValue(config.model); - qrTemplate.setValue(config.templateName); - qrUrl.setValue(config.url); - qrCustomEndpoint.setValue(config.customEndpoint); - } - - writeSettings(); -} - class GeneralSettingsPage : public Core::IOptionsPage { public: @@ -1012,6 +156,7 @@ public: }; const GeneralSettingsPage generalSettingsPage; + /*! \sa {Core::ICore::showOptionsDialog()}, {Core::ICore::showSettings()} \note This function was added to fix Qt Creator API broken changes in v19.0.0-beta2 version @@ -1024,6 +169,7 @@ void showSettings(const Utils::Id page) Core::ICore::showOptionsDialog(page); #endif } + /*! \sa {Core::ICore::showOptionsDialog()}, {Core::ICore::showSettings()} \note This function was added to fix Qt Creator API broken changes in v19.0.0-beta2 version @@ -1037,4 +183,244 @@ void showSettings(const Utils::Id page, Utils::Id item) #endif } +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, + const QPointer &navigator, + const QPointer &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 m_agentFactory; + QPointer m_navigator; + QPointer 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 factoryPtr(agentFactory); + const QPointer navPtr(navigator); + const QPointer agentsNavPtr(agentsNavigator); + setWidgetCreator([factoryPtr, navPtr, agentsNavPtr] { + return new AgentPipelinesPageWidget(factoryPtr, navPtr, agentsNavPtr); + }); + } +}; + +} // namespace + +std::unique_ptr createAgentPipelinesSettingsPage( + AgentFactory *agentFactory, + AgentPipelinesPageNavigator *navigator, + AgentsPageNavigator *agentsNavigator) +{ + return std::make_unique( + agentFactory, navigator, agentsNavigator); +} + } // namespace QodeAssist::Settings + +#include "GeneralSettings.moc" diff --git a/settings/GeneralSettings.hpp b/settings/GeneralSettings.hpp index abc1cd0..e9d8032 100644 --- a/settings/GeneralSettings.hpp +++ b/settings/GeneralSettings.hpp @@ -4,19 +4,23 @@ #pragma once +#include + +#include +#include + #include -#include #include "ButtonAspect.hpp" -#include "ConfigurationManager.hpp" -namespace Utils { -class DetailsWidget; +namespace Core { +class IOptionsPage; } -namespace QodeAssist::PluginLLMCore { -class Provider; +namespace QodeAssist { +class AgentFactory; } + namespace QodeAssist::Settings { class GeneralSettings : public Utils::AspectContainer @@ -33,139 +37,9 @@ public: ButtonAspect checkUpdate{this}; ButtonAspect resetToDefaults{this}; - // code completion setttings - Utils::SelectionAspect ccPresetConfig{this}; - ButtonAspect ccConfigureApiKey{this}; - - Utils::StringAspect ccProvider{this}; - ButtonAspect ccSelectProvider{this}; - - Utils::StringAspect ccModel{this}; - ButtonAspect ccSelectModel{this}; - - Utils::StringAspect ccTemplate{this}; - ButtonAspect ccSelectTemplate{this}; - - Utils::StringAspect ccUrl{this}; - ButtonAspect ccSetUrl{this}; - - Utils::StringAspect ccCustomEndpoint{this}; - - Utils::StringAspect ccStatus{this}; - ButtonAspect ccTest{this}; - - Utils::StringAspect ccTemplateDescription{this}; - - ButtonAspect ccSaveConfig{this}; - ButtonAspect ccLoadConfig{this}; - ButtonAspect ccOpenConfigFolder{this}; - - // TODO create dynamic presets system - // preset1 for code completion settings - Utils::BoolAspect specifyPreset1{this}; - Utils::SelectionAspect preset1Language{this}; - - Utils::StringAspect ccPreset1Provider{this}; - ButtonAspect ccPreset1SelectProvider{this}; - - Utils::StringAspect ccPreset1Url{this}; - ButtonAspect ccPreset1SetUrl{this}; - - Utils::StringAspect ccPreset1CustomEndpoint{this}; - - Utils::StringAspect ccPreset1Model{this}; - ButtonAspect ccPreset1SelectModel{this}; - - Utils::StringAspect ccPreset1Template{this}; - ButtonAspect ccPreset1SelectTemplate{this}; - - // chat assistant settings - Utils::SelectionAspect caPresetConfig{this}; - ButtonAspect caConfigureApiKey{this}; - - Utils::StringAspect caProvider{this}; - ButtonAspect caSelectProvider{this}; - - Utils::StringAspect caModel{this}; - ButtonAspect caSelectModel{this}; - - Utils::StringAspect caTemplate{this}; - ButtonAspect caSelectTemplate{this}; - - Utils::StringAspect caUrl{this}; - ButtonAspect caSetUrl{this}; - - Utils::StringAspect caCustomEndpoint{this}; - - Utils::StringAspect caStatus{this}; - ButtonAspect caTest{this}; - - Utils::StringAspect caTemplateDescription{this}; - - ButtonAspect caSaveConfig{this}; - ButtonAspect caLoadConfig{this}; - ButtonAspect caOpenConfigFolder{this}; - - // quick refactor settings - Utils::SelectionAspect qrPresetConfig{this}; - ButtonAspect qrConfigureApiKey{this}; - - Utils::StringAspect qrProvider{this}; - ButtonAspect qrSelectProvider{this}; - - Utils::StringAspect qrModel{this}; - ButtonAspect qrSelectModel{this}; - - Utils::StringAspect qrTemplate{this}; - ButtonAspect qrSelectTemplate{this}; - - Utils::StringAspect qrUrl{this}; - ButtonAspect qrSetUrl{this}; - - Utils::StringAspect qrCustomEndpoint{this}; - - Utils::StringAspect qrStatus{this}; - ButtonAspect qrTest{this}; - - Utils::StringAspect qrTemplateDescription{this}; - - ButtonAspect qrSaveConfig{this}; - ButtonAspect qrLoadConfig{this}; - ButtonAspect qrOpenConfigFolder{this}; - - ButtonAspect ccShowTemplateInfo{this}; - ButtonAspect caShowTemplateInfo{this}; - ButtonAspect qrShowTemplateInfo{this}; - - void showSelectionDialog( - const QStringList &data, - Utils::StringAspect &aspect, - const QString &title = {}, - const QString &text = {}); - - void showModelsNotFoundDialog(Utils::StringAspect &aspect); - - void showModelsNotSupportedDialog(Utils::StringAspect &aspect); - - void showUrlSelectionDialog(Utils::StringAspect &aspect, const QStringList &predefinedUrls); - - void showTemplateInfoDialog(const Utils::StringAspect &descriptionAspect, const QString &templateName); - - void updatePreset1Visiblity(bool state); - - void onSaveConfiguration(const QString &prefix); - void onLoadConfiguration(const QString &prefix); - - void loadPresetConfigurations(Utils::SelectionAspect &aspect, ConfigurationType type); - void applyPresetConfiguration(int index, ConfigurationType type); - private: void setupConnections(); void resetPageToDefaults(); - - QVector m_ccPresets; - QVector m_caPresets; - QVector m_qrPresets; }; GeneralSettings &generalSettings(); @@ -173,4 +47,22 @@ GeneralSettings &generalSettings(); void showSettings(const Utils::Id page); 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 createAgentPipelinesSettingsPage( + AgentFactory *agentFactory, + AgentPipelinesPageNavigator *navigator, + AgentsPageNavigator *agentsNavigator); + } // namespace QodeAssist::Settings diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 57f2c97..1cc7c96 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -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_USE_SYSTEM_PROMPT[] = "QodeAssist.ccUseSystemPrompt"; const char CC_SYSTEM_PROMPT[] = "QodeAssist.ccSystemPrompt"; -const char CC_SYSTEM_PROMPT_FOR_NON_FIM[] = "QodeAssist.ccSystemPromptForNonFim"; -const char CC_USE_USER_TEMPLATE[] = "QodeAssist.ccUseUserTemplate"; -const char CC_USER_TEMPLATE[] = "QodeAssist.ccUserTemplate"; const char CC_USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.ccUseProjectChangesCache"; const char CC_MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.ccMaxChangesCacheSize"; const char CA_USE_SYSTEM_PROMPT[] = "QodeAssist.useChatSystemPrompt"; const char CA_SYSTEM_PROMPT[] = "QodeAssist.chatSystemPrompt"; -// preset prompt settings -const char CC_TEMPERATURE[] = "QodeAssist.ccTemperature"; -const char CC_MAX_TOKENS[] = "QodeAssist.ccMaxTokens"; -const char CC_USE_TOP_P[] = "QodeAssist.ccUseTopP"; -const char CC_TOP_P[] = "QodeAssist.ccTopP"; -const char CC_USE_TOP_K[] = "QodeAssist.ccUseTopK"; -const char CC_TOP_K[] = "QodeAssist.ccTopK"; -const char CC_USE_PRESENCE_PENALTY[] = "QodeAssist.ccUsePresencePenalty"; -const char CC_PRESENCE_PENALTY[] = "QodeAssist.ccPresencePenalty"; -const char CC_USE_FREQUENCY_PENALTY[] = "QodeAssist.fimUseFrequencyPenalty"; -const char CC_FREQUENCY_PENALTY[] = "QodeAssist.fimFrequencyPenalty"; -const char CC_OLLAMA_LIVETIME[] = "QodeAssist.fimOllamaLivetime"; -const char CC_OLLAMA_CONTEXT_WINDOW[] = "QodeAssist.ccOllamaContextWindow"; - -// OpenAI Responses API Settings -const char CC_OPENAI_RESPONSES_REASONING_EFFORT[] = "QodeAssist.ccOpenAIResponsesReasoningEffort"; - const char CA_TEMPERATURE[] = "QodeAssist.chatTemperature"; const char CA_MAX_TOKENS[] = "QodeAssist.chatMaxTokens"; const char CA_USE_TOP_P[] = "QodeAssist.chatUseTopP"; diff --git a/sources/CMakeLists.txt b/sources/CMakeLists.txt index aad5078..b79ea7d 100644 --- a/sources/CMakeLists.txt +++ b/sources/CMakeLists.txt @@ -5,5 +5,6 @@ add_subdirectory(common) add_subdirectory(providers) add_subdirectory(templates) add_subdirectory(agents) +add_subdirectory(Session) add_subdirectory(providersConfig) add_subdirectory(settings) diff --git a/sources/Session/Session.cpp b/sources/Session/Session.cpp index 83bf52d..c147aab 100644 --- a/sources/Session/Session.cpp +++ b/sources/Session/Session.cpp @@ -106,6 +106,19 @@ bool Session::isInFlight() const noexcept 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) { m_contentLoader = std::move(loader); @@ -186,6 +199,13 @@ LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx) auto *tmpl = m_agent->promptTemplate(); 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}}; if (!provider->prepareRequest(payload, tmpl, ctx, /*tools=*/false, /*thinking=*/false)) return {}; diff --git a/sources/Session/Session.hpp b/sources/Session/Session.hpp index 7f2e49b..7f484b1 100644 --- a/sources/Session/Session.hpp +++ b/sources/Session/Session.hpp @@ -55,6 +55,9 @@ public: ConversationHistory *history() const noexcept { return m_history; } SystemPromptBuilder *systemPrompt() const noexcept { return m_systemPrompt; } + LLMQore::BaseClient *client() const noexcept; + bool supportsImages() const noexcept; + void setContextBindings(Templates::ContextRenderer::Bindings bindings); QString renderAgentContext() const; diff --git a/sources/providers/CMakeLists.txt b/sources/providers/CMakeLists.txt index ce2e329..1a0d40f 100644 --- a/sources/providers/CMakeLists.txt +++ b/sources/providers/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(Providers STATIC ProviderID.hpp Provider.hpp Provider.cpp ProviderFactory.hpp ProviderFactory.cpp + GenericProvider.hpp GenericProvider.cpp ) target_link_libraries(Providers diff --git a/sources/providers/GenericProvider.cpp b/sources/providers/GenericProvider.cpp new file mode 100644 index 0000000..e2bcc37 --- /dev/null +++ b/sources/providers/GenericProvider.cpp @@ -0,0 +1,113 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "GenericProvider.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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> GenericProvider::getInstalledModels(const QString &url) +{ + m_client->setUrl(url); + m_client->setApiKey(apiKey()); + return m_client->listModels(); +} + +namespace { + +using Cap = ProviderCapability; + +template +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 diff --git a/sources/providers/GenericProvider.hpp b/sources/providers/GenericProvider.hpp new file mode 100644 index 0000000..e49c40d --- /dev/null +++ b/sources/providers/GenericProvider.hpp @@ -0,0 +1,50 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#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> 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 diff --git a/sources/settings/AgentPipelinesPage.cpp b/sources/settings/AgentPipelinesPage.cpp deleted file mode 100644 index 31400d1..0000000 --- a/sources/settings/AgentPipelinesPage.cpp +++ /dev/null @@ -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 -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../../Version.hpp" -#include "AgentRosterWidget.hpp" -#include "AgentsSettingsPage.hpp" -#include "Logger.hpp" -#include "PipelinesConfig.hpp" -#include "SettingsConstants.hpp" -#include "SettingsTr.hpp" - -#include - -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, - const QPointer &navigator, - const QPointer &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 m_agentFactory; - QPointer m_navigator; - QPointer 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 factoryPtr(agentFactory); - const QPointer navPtr(navigator); - const QPointer agentsNavPtr(agentsNavigator); - setWidgetCreator([factoryPtr, navPtr, agentsNavPtr] { - return new AgentPipelinesPageWidget(factoryPtr, navPtr, agentsNavPtr); - }); - } -}; - -} // namespace - -std::unique_ptr createAgentPipelinesSettingsPage( - AgentFactory *agentFactory, - AgentPipelinesPageNavigator *navigator, - AgentsPageNavigator *agentsNavigator) -{ - return std::make_unique( - agentFactory, navigator, agentsNavigator); -} - -} // namespace QodeAssist::Settings - -#include "AgentPipelinesPage.moc" diff --git a/sources/settings/AgentPipelinesPage.hpp b/sources/settings/AgentPipelinesPage.hpp deleted file mode 100644 index 17b6afe..0000000 --- a/sources/settings/AgentPipelinesPage.hpp +++ /dev/null @@ -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 - -#include -#include - -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 createAgentPipelinesSettingsPage( - AgentFactory *agentFactory, - AgentPipelinesPageNavigator *navigator, - AgentsPageNavigator *agentsNavigator); - -} // namespace QodeAssist::Settings diff --git a/sources/settings/CMakeLists.txt b/sources/settings/CMakeLists.txt index 95d8660..b7ac298 100644 --- a/sources/settings/CMakeLists.txt +++ b/sources/settings/CMakeLists.txt @@ -1,5 +1,4 @@ -add_library(QodeAssistAgentPipelines OBJECT - AgentPipelinesPage.hpp AgentPipelinesPage.cpp +add_library(QodeAssistAgentPipelines STATIC PipelinesConfig.hpp PipelinesConfig.cpp AgentRosterWidget.hpp AgentRosterWidget.cpp AgentSlotWidget.hpp AgentSlotWidget.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3659261..8140f69 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,5 @@ add_executable(QodeAssistTest ../CodeHandler.cpp - ../LLMClientInterface.cpp ../LLMSuggestion.cpp CodeHandlerTest.cpp ClaudeCacheControlTest.cpp diff --git a/widgets/QuickRefactorDialog.cpp b/widgets/QuickRefactorDialog.cpp index 50f5306..4cf2f82 100644 --- a/widgets/QuickRefactorDialog.cpp +++ b/widgets/QuickRefactorDialog.cpp @@ -7,7 +7,6 @@ #include "CustomInstructionsManager.hpp" #include "QodeAssisttr.h" -#include "settings/ConfigurationManager.hpp" #include "settings/GeneralSettings.hpp" #include "settings/QuickRefactorSettings.hpp" #include "settings/SettingsConstants.hpp" @@ -113,11 +112,6 @@ void QuickRefactorDialog::setupUi() actionsLayout->addWidget(m_alternativeButton); 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(); QColor iconColor = theme ? theme->color(Utils::Theme::TextColorNormal) : QColor(Qt::white); @@ -244,13 +238,6 @@ void QuickRefactorDialog::setupUi() &QuickRefactorDialog::onOpenInstructionsFolder); loadCustomCommands(); - loadAvailableConfigurations(); - - connect( - m_configComboBox, - QOverload::of(&QComboBox::currentIndexChanged), - this, - &QuickRefactorDialog::onConfigurationChanged); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); @@ -577,60 +564,6 @@ void QuickRefactorDialog::onOpenSettings() 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 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() { QString instruction = m_instructionEdit->toPlainText().trimmed(); diff --git a/widgets/QuickRefactorDialog.hpp b/widgets/QuickRefactorDialog.hpp index 96999d6..cc24844 100644 --- a/widgets/QuickRefactorDialog.hpp +++ b/widgets/QuickRefactorDialog.hpp @@ -33,8 +33,6 @@ public: Action selectedAction() const; - QString selectedConfiguration() const; - bool eventFilter(QObject *watched, QEvent *event) override; void keyPressEvent(QKeyEvent *event) override; @@ -50,8 +48,6 @@ private slots: void onOpenInstructionsFolder(); void onOpenSettings(); void loadCustomCommands(); - void loadAvailableConfigurations(); - void onConfigurationChanged(int index); void validateAndAccept(); private: @@ -71,12 +67,10 @@ private: QToolButton *m_toolsButton; QToolButton *m_thinkingButton; QComboBox *m_commandsComboBox; - QComboBox *m_configComboBox; Action m_selectedAction = Action::Custom; QString m_lastInstructions; - QString m_selectedConfiguration; - + QIcon m_toolsIconOn; QIcon m_toolsIconOff; QIcon m_thinkingIconOn;