Compare commits

..

18 Commits

Author SHA1 Message Date
Petr Mironychev
35bbaa1af0 fix: Found and fix review mistakes 2026-07-02 22:26:32 +02:00
Petr Mironychev
c070d65366 fix: Disable shown agent without selecting 2026-06-30 13:45:48 +02:00
Petr Mironychev
714b1367b7 feat: Improve list in agent settngs list 2026-06-30 11:52:59 +02:00
Petr Mironychev
f688b53703 refactor: Move to 0.7.0 version of llmqore 2026-06-30 10:23:17 +02:00
Petr Mironychev
2a3fd4f5be feat: Improve agents config 2026-06-29 23:50:27 +02:00
Petr Mironychev
080947c0dc feat: Add possibility to overwrite agent config tools enabling 2026-06-29 18:32:45 +02:00
Petr Mironychev
9cc57c602b feat: Add OpenAI agents config 2026-06-29 15:42:02 +02:00
Petr Mironychev
4e3ecdd1f6 feat: Add mistral agents configs 2026-06-29 15:12:02 +02:00
Petr Mironychev
86135d0c13 fix: Remove files and folder watches 2026-06-29 15:11:40 +02:00
Petr Mironychev
d66c714a28 fix: Merging tool result 2026-06-29 13:28:26 +02:00
Petr Mironychev
86c537477d refactor: Provider settings panel improve 2026-06-29 10:42:33 +02:00
Petr Mironychev
70c6d30a72 feat: Add more agents configs 2026-06-29 10:05:28 +02:00
Petr Mironychev
747dfb540e fix: Add new copyrights 2026-06-29 00:44:13 +02:00
Petr Mironychev
e200278f9a fix: Move Quick Refactor agent to toml file 2026-06-29 00:27:01 +02:00
Petr Mironychev
755263c4de fix: Improve agents profiles for Ollama 2026-06-28 23:37:28 +02:00
Petr Mironychev
a6921f523a fix: Remove token calibration 2026-06-28 22:10:29 +02:00
Petr Mironychev
dc3100f054 fix: Remove isUserSource from tests 2026-06-28 21:55:47 +02:00
Petr Mironychev
ccc2ec2e80 refactor: Move to agent architecture 2026-06-28 17:38:08 +02:00
444 changed files with 13737 additions and 20117 deletions

View File

@@ -2,10 +2,6 @@ cmake_minimum_required(VERSION 3.16)
project(QodeAssist) project(QodeAssist)
option(QODEASSIST_EXPERIMENTAL
"Enable experimental features" OFF)
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
@@ -42,7 +38,6 @@ add_definitions(
add_subdirectory(sources) add_subdirectory(sources)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(pluginllmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(UIControls) add_subdirectory(UIControls)
add_subdirectory(ChatView) add_subdirectory(ChatView)
@@ -69,7 +64,6 @@ add_qtc_plugin(QodeAssist
QtCreator::Utils QtCreator::Utils
QtCreator::CPlusPlus QtCreator::CPlusPlus
LLMQore LLMQore
PluginLLMCore
ProvidersConfig ProvidersConfig
Agents Agents
Skills Skills
@@ -83,42 +77,6 @@ add_qtc_plugin(QodeAssist
QodeAssisttr.h QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
RefactorContextHelper.hpp RefactorContextHelper.hpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp
templates/Llama3.hpp
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
templates/Qwen3CoderFIM.hpp
templates/OpenAIResponses.hpp
providers/Providers.hpp
providers/ProviderUrlUtils.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
providers/QwenProvider.hpp providers/QwenProvider.cpp
providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp
providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
@@ -130,17 +88,12 @@ add_qtc_plugin(QodeAssist
chat/ChatDocument.hpp chat/ChatDocument.cpp chat/ChatDocument.hpp chat/ChatDocument.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp
widgets/CompletionHintWidget.hpp widgets/CompletionHintWidget.cpp
widgets/CompletionHintHandler.hpp widgets/CompletionHintHandler.cpp
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
@@ -170,10 +123,7 @@ add_qtc_plugin(QodeAssist
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
) )
if(QODEASSIST_EXPERIMENTAL) target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
endif()
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable find_program(QtCreatorExecutable

View File

@@ -1,124 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "AgentRoleController.hpp"
#include <utils/aspects.h>
#include "AgentRole.hpp"
#include "ChatAssistantSettings.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat {
AgentRoleController::AgentRoleController(QObject *parent)
: QObject(parent)
{
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&AgentRoleController::baseSystemPromptChanged);
loadAvailableRoles();
}
QStringList AgentRoleController::availableRoles() const
{
return m_availableRoles;
}
QString AgentRoleController::currentRole() const
{
return m_currentRole;
}
QString AgentRoleController::baseSystemPrompt() const
{
return Settings::chatAssistantSettings().systemPrompt();
}
QString AgentRoleController::currentRoleDescription() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
return role.description;
}
QString AgentRoleController::currentRoleSystemPrompt() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return QString();
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return QString();
return role.systemPrompt;
}
void AgentRoleController::loadAvailableRoles()
{
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
m_availableRoles.clear();
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
for (const auto &role : roles)
m_availableRoles.append(role.name);
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
if (!lastRoleId.isEmpty()) {
for (const auto &role : roles) {
if (role.id == lastRoleId) {
m_currentRole = role.name;
break;
}
}
}
emit availableRolesChanged();
emit currentRoleChanged();
}
void AgentRoleController::applyRole(const QString &roleName)
{
auto &settings = Settings::chatAssistantSettings();
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
settings.lastUsedRoleId.setValue("");
settings.writeSettings();
m_currentRole = roleName;
emit currentRoleChanged();
return;
}
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
for (const auto &role : roles) {
if (role.name == roleName) {
settings.lastUsedRoleId.setValue(role.id);
settings.writeSettings();
m_currentRole = role.name;
emit currentRoleChanged();
break;
}
}
}
void AgentRoleController::openSettings()
{
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
}
} // namespace QodeAssist::Chat

View File

@@ -1,39 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QStringList>
namespace QodeAssist::Chat {
class AgentRoleController : public QObject
{
Q_OBJECT
public:
explicit AgentRoleController(QObject *parent = nullptr);
QStringList availableRoles() const;
QString currentRole() const;
QString baseSystemPrompt() const;
QString currentRoleDescription() const;
QString currentRoleSystemPrompt() const;
void loadAvailableRoles();
void applyRole(const QString &roleName);
void openSettings();
signals:
void availableRolesChanged();
void currentRoleChanged();
void baseSystemPromptChanged();
private:
QStringList m_availableRoles;
QString m_currentRole;
};
} // namespace QodeAssist::Chat

View File

@@ -22,7 +22,6 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/BottomBar.qml qml/controls/BottomBar.qml
qml/controls/FileMentionPopup.qml qml/controls/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml qml/controls/FileEditsActionBar.qml
qml/controls/ContextViewer.qml
qml/controls/SkillCommandPopup.qml qml/controls/SkillCommandPopup.qml
qml/controls/Toast.qml qml/controls/Toast.qml
qml/controls/TopBar.qml qml/controls/TopBar.qml
@@ -47,8 +46,6 @@ qt_add_qml_module(QodeAssistChatView
icons/chat-pause-icon.svg icons/chat-pause-icon.svg
icons/warning-icon.svg icons/warning-icon.svg
icons/new-chat-icon.svg icons/new-chat-icon.svg
icons/rules-icon.svg
icons/context-icon.svg
icons/open-in-editor.svg icons/open-in-editor.svg
icons/open-in-window.svg icons/open-in-window.svg
icons/apply-changes-button.svg icons/apply-changes-button.svg
@@ -75,8 +72,7 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp ChatCompressor.hpp ChatCompressor.cpp
AgentRoleController.hpp AgentRoleController.cpp ChatAgentController.hpp ChatAgentController.cpp
ChatConfigurationController.hpp ChatConfigurationController.cpp
FileEditController.hpp FileEditController.cpp FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp ChatHistoryStore.hpp ChatHistoryStore.cpp
@@ -92,13 +88,14 @@ target_link_libraries(QodeAssistChatView
Qt::Network Qt::Network
QtCreator::Core QtCreator::Core
QtCreator::Utils QtCreator::Utils
PluginLLMCore
QodeAssistSettings QodeAssistSettings
Context Context
QodeAssistUIControlsplugin QodeAssistUIControlsplugin
QodeAssistLogger QodeAssistLogger
LLMQore LLMQore
Skills Skills
Agents
Session
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView

View File

@@ -0,0 +1,114 @@
// 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 "ChatAgentController.hpp"
#include <QSettings>
#include <coreplugin/icore.h>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <sources/settings/PipelinesConfig.hpp>
namespace QodeAssist::Chat {
namespace {
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
}
ChatAgentController::ChatAgentController(QObject *parent)
: QObject(parent)
{
if (auto *settings = Core::ICore::settings())
m_currentAgent = settings->value(kChatAgentKey).toString();
connect(
Settings::PipelinesNotifier::instance(),
&Settings::PipelinesNotifier::pipelinesChanged,
this,
&ChatAgentController::reload);
}
void ChatAgentController::setAgentFactory(AgentFactory *factory)
{
m_agentFactory = factory;
if (factory)
connect(
factory,
&AgentFactory::agentsChanged,
this,
&ChatAgentController::reload,
Qt::UniqueConnection);
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;
applyCurrentAgent(name);
}
void ChatAgentController::reload()
{
if (!m_agentFactory)
return;
const QStringList all = m_agentFactory->configNames();
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatAssistant;
QStringList filtered;
for (const QString &name : roster) {
if (all.contains(name))
filtered.append(name);
}
if (filtered != m_availableAgents) {
m_availableAgents = filtered;
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;
applyCurrentAgent(next);
}
void ChatAgentController::applyCurrentAgent(const QString &name)
{
m_currentAgent = name;
if (auto *settings = Core::ICore::settings())
settings->setValue(kChatAgentKey, m_currentAgent);
emit currentAgentChanged();
}
bool ChatAgentController::currentSupportsTools() const
{
if (!m_agentFactory || m_currentAgent.isEmpty())
return false;
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
return config && config->enableTools;
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,48 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QPointer>
#include <QString>
#include <QStringList>
namespace QodeAssist {
class AgentFactory;
}
namespace QodeAssist::Chat {
class ChatAgentController : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(ChatAgentController)
public:
explicit ChatAgentController(QObject *parent = nullptr);
void setAgentFactory(AgentFactory *factory);
[[nodiscard]] QStringList availableAgents() const;
[[nodiscard]] QString currentAgent() const;
void setCurrentAgent(const QString &name);
[[nodiscard]] bool currentSupportsTools() const;
signals:
void availableAgentsChanged();
void currentAgentChanged();
private:
void reload();
void ensureValidCurrent();
void applyCurrentAgent(const QString &name);
QPointer<AgentFactory> m_agentFactory;
QStringList m_availableAgents;
QString m_currentAgent;
};
} // namespace QodeAssist::Chat

View File

@@ -4,19 +4,32 @@
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include "ChatModel.hpp" #include <LLMQore/ContentBlocks.hpp>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include <AgentFactory.hpp>
#include <ContextRenderer.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <QDateTime> #include <QDateTime>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QStringList>
#include <QUuid> #include <QUuid>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -25,7 +38,17 @@ ChatCompressor::ChatCompressor(QObject *parent)
: QObject(parent) : QObject(parent)
{} {}
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel) void ChatCompressor::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void ChatCompressor::setActiveAgent(const QString &agentName)
{
m_activeAgent = agentName;
}
void ChatCompressor::startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory)
{ {
if (m_isCompressing) { if (m_isCompressing) {
emit compressionFailed(tr("Compression already in progress")); emit compressionFailed(tr("Compression already in progress"));
@@ -37,49 +60,85 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
return; return;
} }
if (!chatModel || chatModel->rowCount() == 0) { if (!sourceHistory || sourceHistory->isEmpty()) {
emit compressionFailed(tr("Chat is empty, nothing to compress")); emit compressionFailed(tr("Chat is empty, nothing to compress"));
return; return;
} }
auto providerName = Settings::generalSettings().caProvider(); if (!m_sessionManager) {
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); emit compressionFailed(tr("Chat session manager is not available"));
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
return; return;
} }
auto templateName = Settings::generalSettings().caTemplate(); QString sessionError;
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName( Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
templateName); if (!session) {
emit compressionFailed(sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
if (!promptTemplate) {
emit compressionFailed(tr("No template available"));
return; return;
} }
auto *client = session->client();
if (!client) {
m_sessionManager->removeSession(session);
emit compressionFailed(tr("Chat agent has no live client"));
return;
}
auto *project = ProjectExplorer::ProjectManager::startupProject();
Templates::ContextRenderer::Bindings bindings;
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
bindings.configDir = AgentFactory::userConfigDir();
session->setContextBindings(bindings);
m_isCompressing = true; m_isCompressing = true;
m_chatModel = chatModel;
m_originalChatPath = chatFilePath; m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear(); m_session = session;
emit compressionStarted(); emit compressionStarted();
connectProviderSignals(); QStringList transcriptParts;
for (const auto &msg : sourceHistory->messages()) {
if (msg.role() != Message::Role::User && msg.role() != Message::Role::Assistant)
continue;
const QString text = msg.text();
if (text.trimmed().isEmpty())
continue;
QJsonObject payload{ const QString role = msg.role() == Message::Role::User ? QStringLiteral("User")
{"model", Settings::generalSettings().caModel()}, {"stream", true}}; : QStringLiteral("Assistant");
transcriptParts.append(QStringLiteral("%1: %2").arg(role, text));
}
buildRequestPayload(payload, promptTemplate); if (transcriptParts.isEmpty()) {
handleCompressionError(tr("Chat is empty, nothing to compress"));
return;
}
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint(); const QString transcript = transcriptParts.join(QStringLiteral("\n\n"));
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint(); connect(session, &Session::finished, this, [this](const LLMQore::RequestID &id, const QString &) {
m_provider->client()->setTransferTimeout( onCompressionFinished(id);
});
connect(
session,
&Session::failed,
this,
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onCompressionFailed(id, error.message);
});
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_currentRequestId = m_provider->sendRequest(
QUrl(Settings::generalSettings().caUrl()), payload, endpoint); std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<LLMQore::TextContent>(transcript));
m_currentRequestId = session->send(std::move(blocks));
if (m_currentRequestId.isEmpty()) {
handleCompressionError(
tr("Failed to start compression request: %1").arg(session->lastError().message));
return;
}
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId)); LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
} }
@@ -94,44 +153,43 @@ void ChatCompressor::cancelCompression()
return; return;
LOG_MESSAGE("Cancelling compression request"); LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState(); cleanupState();
emit compressionFailed(tr("Compression cancelled")); emit compressionFailed(tr("Compression cancelled"));
} }
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText) void ChatCompressor::onCompressionFinished(const QString &requestId)
{ {
if (!m_isCompressing || requestId != m_currentRequestId) if (!m_isCompressing || requestId != m_currentRequestId)
return; return;
m_accumulatedSummary += partialText; QString summary;
} if (m_session) {
if (auto *history = m_session->history(); history && !history->isEmpty())
summary = history->messages().back().text();
}
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText) LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
{
Q_UNUSED(fullText)
if (!m_isCompressing || requestId != m_currentRequestId) if (summary.trimmed().isEmpty()) {
handleCompressionError(tr("Compression produced an empty summary"));
return; return;
}
LOG_MESSAGE( const QString compressedPath = createCompressedChatPath(m_originalChatPath);
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length())); const QString sourcePath = m_originalChatPath;
QString compressedPath = createCompressedChatPath(m_originalChatPath); cleanupState();
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
handleCompressionError(tr("Failed to save compressed chat")); if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
emit compressionFailed(tr("Failed to save compressed chat"));
return; return;
} }
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath)); LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
emit compressionCompleted(compressedPath); emit compressionCompleted(compressedPath);
} }
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error) void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error)
{ {
if (!m_isCompressing || requestId != m_currentRequestId) if (!m_isCompressing || requestId != m_currentRequestId)
return; return;
@@ -154,81 +212,19 @@ QString ChatCompressor::createCompressedChatPath(const QString &originalPath) co
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix()); .arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
} }
QString ChatCompressor::buildCompressionPrompt() const
{
return QStringLiteral(
"Please create a comprehensive summary of our entire conversation above. "
"The summary should:\n"
"1. Preserve all important context, decisions, and key information\n"
"2. Maintain technical details, code snippets, file references, and specific examples\n"
"3. Keep the chronological flow of the discussion\n"
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
"5. Be written in clear, structured format\n"
"6. Use markdown formatting for better readability\n\n"
"Create the summary now:");
}
void ChatCompressor::buildRequestPayload(
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
{
PluginLLMCore::ContextData context;
context.systemPrompt = QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of discussion.");
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool
|| msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
PluginLLMCore::Message apiMessage;
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
apiMessage.content = msg.content;
messages.append(apiMessage);
}
PluginLLMCore::Message compressionRequest;
compressionRequest.role = "user";
compressionRequest.content = buildCompressionPrompt();
messages.append(compressionRequest);
context.history = messages;
m_provider->prepareRequest(
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
}
bool ChatCompressor::createCompressedChatFile( bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary) const QString &sourcePath, const QString &destPath, const QString &summary)
{ {
QFile sourceFile(sourcePath); QJsonObject root;
if (!sourceFile.open(QIODevice::ReadOnly)) { root["version"] = ChatSerializer::VERSION;
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
return false;
}
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
sourceFile.close();
if (doc.isNull() || !doc.isObject()) {
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
.arg(sourcePath, parseError.errorString()));
return false;
}
QJsonObject root = doc.object();
QJsonObject summaryMessage; QJsonObject summaryMessage;
summaryMessage["role"] = "assistant"; summaryMessage["role"] = "assistant";
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces); summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
summaryMessage["isRedacted"] = false; QJsonObject textBlock;
summaryMessage["attachments"] = QJsonArray(); textBlock["type"] = "text";
summaryMessage["images"] = QJsonArray(); textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["blocks"] = QJsonArray{textBlock};
root["messages"] = QJsonArray{summaryMessage}; root["messages"] = QJsonArray{summaryMessage};
root["compressedFrom"] = sourcePath; root["compressedFrom"] = sourcePath;
@@ -247,49 +243,17 @@ bool ChatCompressor::createCompressedChatFile(
return true; return true;
} }
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));
}
void ChatCompressor::disconnectAllSignals()
{
for (const auto &connection : std::as_const(m_connections))
disconnect(connection);
m_connections.clear();
}
void ChatCompressor::cleanupState() void ChatCompressor::cleanupState()
{ {
disconnectAllSignals(); Session *session = m_session;
m_isCompressing = false; m_isCompressing = false;
m_currentRequestId.clear(); m_currentRequestId.clear();
m_originalChatPath.clear(); m_originalChatPath.clear();
m_accumulatedSummary.clear(); m_session = nullptr;
m_chatModel = nullptr;
m_provider = nullptr; if (session && m_sessionManager)
m_sessionManager->release(session);
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -4,20 +4,19 @@
#pragma once #pragma once
#include <QJsonObject>
#include <QList> #include <QList>
#include <QObject> #include <QObject>
#include <QPointer>
#include <QString> #include <QString>
namespace QodeAssist::PluginLLMCore { namespace QodeAssist {
class Provider; class SessionManager;
class PromptTemplate; class Session;
} // namespace QodeAssist::PluginLLMCore class ConversationHistory;
} // namespace QodeAssist
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject class ChatCompressor : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -25,7 +24,10 @@ class ChatCompressor : public QObject
public: public:
explicit ChatCompressor(QObject *parent = nullptr); explicit ChatCompressor(QObject *parent = nullptr);
void startCompression(const QString &chatFilePath, ChatModel *chatModel); void setSessionManager(SessionManager *sessionManager);
void setActiveAgent(const QString &agentName);
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
bool isCompressing() const; bool isCompressing() const;
void cancelCompression(); void cancelCompression();
@@ -35,30 +37,22 @@ signals:
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error); void compressionFailed(const QString &error);
private slots:
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
void onFullResponseReceived(const QString &requestId, const QString &fullText);
void onRequestFailed(const QString &requestId, const QString &error);
private: private:
void onCompressionFinished(const QString &requestId);
void onCompressionFailed(const QString &requestId, const QString &error);
QString createCompressedChatPath(const QString &originalPath) const; QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const;
bool createCompressedChatFile( bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary); const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState(); void cleanupState();
void handleCompressionError(const QString &error); void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false; bool m_isCompressing = false;
QString m_currentRequestId; QString m_currentRequestId;
QString m_originalChatPath; QString m_originalChatPath;
QString m_accumulatedSummary; QPointer<SessionManager> m_sessionManager;
PluginLLMCore::Provider *m_provider = nullptr; QString m_activeAgent;
ChatModel *m_chatModel = nullptr; QPointer<Session> m_session;
QList<QMetaObject::Connection> m_connections;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,100 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatConfigurationController.hpp"
#include <utils/aspects.h>
#include "ConfigurationManager.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat {
ChatConfigurationController::ChatConfigurationController(QObject *parent)
: QObject(parent)
{
auto &settings = Settings::generalSettings();
connect(
&settings.caProvider,
&Utils::BaseAspect::changed,
this,
&ChatConfigurationController::updateCurrentConfiguration);
connect(
&settings.caModel,
&Utils::BaseAspect::changed,
this,
&ChatConfigurationController::updateCurrentConfiguration);
loadAvailableConfigurations();
}
QStringList ChatConfigurationController::availableConfigurations() const
{
return m_availableConfigurations;
}
QString ChatConfigurationController::currentConfiguration() const
{
return m_currentConfiguration;
}
void ChatConfigurationController::updateCurrentConfiguration()
{
auto &settings = Settings::generalSettings();
m_currentConfiguration
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
emit currentConfigurationChanged();
}
void ChatConfigurationController::loadAvailableConfigurations()
{
auto &manager = Settings::ConfigurationManager::instance();
manager.loadConfigurations(Settings::ConfigurationType::Chat);
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::Chat);
m_availableConfigurations.clear();
m_availableConfigurations.append(QObject::tr("Current Settings"));
for (const Settings::AIConfiguration &config : configs) {
m_availableConfigurations.append(config.name);
}
updateCurrentConfiguration();
emit availableConfigurationsChanged();
}
void ChatConfigurationController::applyConfiguration(const QString &configName)
{
if (configName == QObject::tr("Current Settings")) {
return;
}
auto &manager = Settings::ConfigurationManager::instance();
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::Chat);
for (const Settings::AIConfiguration &config : configs) {
if (config.name == configName) {
auto &settings = Settings::generalSettings();
settings.caProvider.setValue(config.provider);
settings.caModel.setValue(config.model);
settings.caTemplate.setValue(config.templateName);
settings.caUrl.setValue(config.url);
settings.caCustomEndpoint.setValue(config.customEndpoint);
settings.writeSettings();
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
emit currentConfigurationChanged();
break;
}
}
}
} // namespace QodeAssist::Chat

View File

@@ -1,36 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QStringList>
namespace QodeAssist::Chat {
class ChatConfigurationController : public QObject
{
Q_OBJECT
public:
explicit ChatConfigurationController(QObject *parent = nullptr);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
void loadAvailableConfigurations();
void applyConfiguration(const QString &configName);
signals:
void availableConfigurationsChanged();
void currentConfigurationChanged();
private:
void updateCurrentConfiguration();
QStringList m_availableConfigurations;
QString m_currentConfiguration;
};
} // namespace QodeAssist::Chat

View File

@@ -16,14 +16,22 @@
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent) ChatHistoryStore::ChatHistoryStore(
ConversationHistory *history, ChatModel *chatModel, QObject *parent)
: QObject(parent) : QObject(parent)
, m_history(history)
, m_chatModel(chatModel) , m_chatModel(chatModel)
{} {}
@@ -52,19 +60,25 @@ QString ChatHistoryStore::suggestedFileName() const
{ {
QString shortMessage; QString shortMessage;
if (m_chatModel->rowCount() > 0) { if (m_history) {
QString firstMessage for (const auto &message : m_history->messages()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString(); if (message.role() != Message::Role::User)
shortMessage = firstMessage.split('\n').first().simplified().left(30); continue;
if (shortMessage.isEmpty()) { const QString text = message.text();
QVariantList images if (!text.trimmed().isEmpty()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList(); shortMessage = text.split('\n').first().simplified().left(30);
if (!images.isEmpty()) { } else {
for (const auto &block : message.blocks()) {
if (dynamic_cast<StoredImageContent *>(block.get())) {
shortMessage = "image_chat"; shortMessage = "image_chat";
break;
} }
} }
} }
break;
}
}
return generateChatFileName(shortMessage, historyDir()); return generateChatFileName(shortMessage, historyDir());
} }
@@ -107,12 +121,17 @@ QString ChatHistoryStore::autosaveFilePath(
SerializationResult ChatHistoryStore::save(const QString &filePath) const SerializationResult ChatHistoryStore::save(const QString &filePath) const
{ {
return ChatSerializer::saveToFile(m_chatModel, filePath); return ChatSerializer::saveToFile(
m_history, filePath, m_chatModel ? m_chatModel->usageToJson() : QJsonObject{});
} }
SerializationResult ChatHistoryStore::load(const QString &filePath) const SerializationResult ChatHistoryStore::load(const QString &filePath) const
{ {
return ChatSerializer::loadFromFile(m_chatModel, filePath); QJsonObject usage;
const SerializationResult result = ChatSerializer::loadFromFile(m_history, filePath, &usage);
if (result.success && m_chatModel)
m_chatModel->restoreUsageFromJson(usage);
return result;
} }
void ChatHistoryStore::showSaveDialog() void ChatHistoryStore::showSaveDialog()

View File

@@ -9,6 +9,10 @@
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel; class ChatModel;
@@ -18,7 +22,8 @@ class ChatHistoryStore : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr); explicit ChatHistoryStore(
ConversationHistory *history, ChatModel *chatModel, QObject *parent = nullptr);
QString historyDir() const; QString historyDir() const;
QString suggestedFileName() const; QString suggestedFileName() const;
@@ -42,6 +47,7 @@ signals:
private: private:
QString generateChatFileName(const QString &shortMessage, const QString &dir) const; QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
ConversationHistory *m_history;
ChatModel *m_chatModel; ChatModel *m_chatModel;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,16 @@
#include "MessagePart.hpp" #include "MessagePart.hpp"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QHash>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QPointer>
#include <QVector>
#include <QtQmlIntegration> #include <QtQmlIntegration>
#include "context/ContentFile.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -21,15 +26,15 @@ class ChatModel : public QAbstractListModel
Q_OBJECT Q_OBJECT
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL) int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking }; enum ChatRole : int { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { enum Roles : int {
RoleType = Qt::UserRole, RoleType = Qt::UserRole,
Content, Content,
Attachments, Attachments,
@@ -43,81 +48,19 @@ public:
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
struct ImageAttachment
{
QString fileName; // Original filename
QString storedPath; // Path to stored image file (relative to chat folder)
QString mediaType; // MIME type
};
struct Message
{
ChatRole role;
QString content;
QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments;
QList<ImageAttachment> images;
QString toolName;
QJsonObject toolArguments;
QString toolResult;
int promptTokens = 0;
int completionTokens = 0;
int cachedPromptTokens = 0;
int reasoningTokens = 0;
};
explicit ChatModel(QObject *parent = nullptr); explicit ChatModel(QObject *parent = nullptr);
void setHistory(ConversationHistory *history);
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {},
bool isRedacted = false,
const QString &signature = QString());
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index); Q_INVOKABLE void resetModelTo(int index);
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const; Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
void addToolExecutionStatus(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments);
void dropTrailingAssistantMessage(const QString &requestId);
void setToolMessageData(
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments,
const QString &toolResult);
void updateToolResult(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &result);
void addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature);
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent);
void setMessageUsage( void setMessageUsage(
const QString &messageId, const QString &messageId,
int promptTokens, int promptTokens,
@@ -125,13 +68,12 @@ public:
int cachedPromptTokens, int cachedPromptTokens,
int reasoningTokens); int reasoningTokens);
QJsonObject usageToJson() const;
void restoreUsageFromJson(const QJsonObject &usage);
int sessionPromptTokens() const; int sessionPromptTokens() const;
int sessionCompletionTokens() const; int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const; int sessionCachedPromptTokens() const;
int sessionTotalTokens() const;
void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
@@ -141,18 +83,64 @@ signals:
void sessionUsageChanged(); void sessionUsageChanged();
private slots: private slots:
void onFileEditApplied(const QString &editId); void onHistoryMessageAdded(int index);
void onFileEditRejected(const QString &editId); void onHistoryMessageUpdated(int index);
void onFileEditArchived(const QString &editId); void onHistoryCleared();
void onHistoryReset();
void onFileEditStatusChanged(const QString &editId);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage); struct AttachmentRef
{
QString fileName;
QString storedPath;
};
struct ImageRef
{
QString fileName;
QString storedPath;
QString mediaType;
};
struct Row
{
ChatRole kind = ChatRole::Assistant;
int messageIndex = -1;
QString messageId;
QString content;
bool isRedacted = false;
QString editId;
QString fileEditDisplay;
QVector<AttachmentRef> attachments;
QVector<ImageRef> images;
};
struct Usage
{
int prompt = 0;
int completion = 0;
int cached = 0;
int reasoning = 0;
};
QVector<Message> m_messages; void rebuildAll();
bool m_loadingFromHistory = false; void reprojectTail(int startMessageIndex);
int startMessageIndexFor(int messageIndex) const;
int firstRowForMessage(int messageIndex) const;
QHash<QString, QString> buildToolResultMap() const;
void mergeToolResultsFromMessage(int messageIndex);
void pruneUsageToHistory();
void appendRowsForMessage(
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
QVariantList buildImageList(const QVector<ImageRef> &images) const;
QPointer<ConversationHistory> m_history;
QVector<Row> m_rows;
QHash<QString, QString> m_toolResults;
QHash<QString, Usage> m_usageByMessageId;
QString m_chatFilePath; QString m_chatFilePath;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart) Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

View File

@@ -28,22 +28,25 @@
#include "QodeAssistConstants.hpp" #include "QodeAssistConstants.hpp"
#include "AgentRoleController.hpp" #include <AgentFactory.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <SessionManager.hpp>
#include <sources/settings/PipelinesConfig.hpp>
#include "ChatAgentController.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatConfigurationController.hpp"
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include "ChatHistoryStore.hpp" #include "ChatHistoryStore.hpp"
#include "FileEditController.hpp" #include "FileEditController.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "InputTokenCounter.hpp" #include "InputTokenCounter.hpp"
#include "SettingsConstants.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp"
#include "pluginllmcore/RulesLoader.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "SessionFileRegistry.hpp"
#include "SettingsConstants.hpp"
#include "SkillsSettings.hpp" #include "SkillsSettings.hpp"
#include "context/ContextManager.hpp"
#include "sources/skills/SkillsManager.hpp" #include "sources/skills/SkillsManager.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -73,19 +76,20 @@ QKeySequence sendMessageKeySequence()
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_history(new QodeAssist::ConversationHistory(this))
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, this))
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_fileManager(new ChatFileManager(this)) , m_fileManager(new ChatFileManager(this))
, m_isRequestInProgress(false) , m_isRequestInProgress(false)
, m_chatCompressor(new ChatCompressor(this)) , m_chatCompressor(new ChatCompressor(this))
, m_agentRoleController(new AgentRoleController(this)) , m_agentController(new ChatAgentController(this))
, m_configurationController(new ChatConfigurationController(this)) , m_fileEditController(new FileEditController(this))
, m_fileEditController(new FileEditController(m_chatModel, this)) , m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
, m_tokenCounter( , m_historyStore(new ChatHistoryStore(m_history, m_chatModel, this))
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
{ {
m_chatModel->setHistory(m_history);
m_clientInterface->setHistory(m_history);
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles(); m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect( connect(
&Settings::chatAssistantSettings().linkOpenFiles, &Settings::chatAssistantSettings().linkOpenFiles,
@@ -109,22 +113,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
}, },
Qt::QueuedConnection); Qt::QueuedConnection);
auto &settings = Settings::generalSettings();
connect(
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
connect(
m_configurationController,
&ChatConfigurationController::availableConfigurationsChanged,
this,
&ChatRootView::availableConfigurationsChanged);
connect(
m_configurationController,
&ChatConfigurationController::currentConfigurationChanged,
this,
&ChatRootView::currentConfigurationChanged);
connect( connect(
m_clientInterface, m_clientInterface,
&ClientInterface::messageReceivedCompletely, &ClientInterface::messageReceivedCompletely,
@@ -143,6 +131,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
setRecentFilePath(QString{}); setRecentFilePath(QString{});
m_tokenCounter->resetServerUsage();
m_fileEditController->clearCurrentRequestId(); m_fileEditController->clearCurrentRequestId();
}); });
auto maybeEmitTitle = [this] { auto maybeEmitTitle = [this] {
@@ -156,7 +145,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(m_chatModel, &QAbstractItemModel::modelReset, this, maybeEmitTitle); connect(m_chatModel, &QAbstractItemModel::modelReset, this, maybeEmitTitle);
connect(m_chatModel, &QAbstractItemModel::rowsInserted, this, maybeEmitTitle); connect(m_chatModel, &QAbstractItemModel::rowsInserted, this, maybeEmitTitle);
connect(m_chatModel, &QAbstractItemModel::rowsRemoved, this, maybeEmitTitle); connect(m_chatModel, &QAbstractItemModel::rowsRemoved, this, maybeEmitTitle);
connect(m_chatModel, &QAbstractItemModel::dataChanged, this, maybeEmitTitle);
connect(this, &ChatRootView::attachmentFilesChanged, this, [this]() { connect(this, &ChatRootView::attachmentFilesChanged, this, [this]() {
m_tokenCounter->setAttachments(m_attachmentFiles); m_tokenCounter->setAttachments(m_attachmentFiles);
}); });
@@ -171,20 +159,28 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::inputTokensCountChanged); &ChatRootView::inputTokensCountChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::availableRolesChanged, &ChatAgentController::availableAgentsChanged,
this, this,
&ChatRootView::availableAgentRolesChanged); &ChatRootView::availableChatAgentsChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::currentRoleChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::currentAgentRoleChanged); &ChatRootView::currentChatAgentChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::baseSystemPromptChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::baseSystemPromptChanged); &ChatRootView::useToolsChanged);
connect(
Settings::PipelinesNotifier::instance(),
&Settings::PipelinesNotifier::pipelinesChanged,
this,
[this]() {
emit availableChatAgentsChanged();
emit useToolsChanged();
});
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@@ -234,17 +230,21 @@ ChatRootView::ChatRootView(QQuickItem *parent)
}); });
connect( connect(
m_clientInterface, m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
&ClientInterface::requestStarted, m_fileEditController->setCurrentRequestId(requestId);
this, setRequestProgressStatus(true);
[this](const QString &requestId) { m_fileEditController->setCurrentRequestId(requestId); }); });
connect(m_clientInterface, &ClientInterface::requestCancelled, this, [this]() {
setRequestProgressStatus(false);
});
connect( connect(
m_clientInterface, m_clientInterface,
&ClientInterface::messageUsageReceived, &ClientInterface::messageUsageReceived,
this, this,
[this](int promptTokens, int /*completionTokens*/, int /*cached*/, int /*reasoning*/) { [this](int promptTokens, int /*completionTokens*/, int cachedTokens, int /*reasoning*/) {
m_tokenCounter->recordServerUsage(promptTokens); m_tokenCounter->recordServerUsage(promptTokens, cachedTokens);
}); });
connect( connect(
@@ -266,14 +266,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect( connect(
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory); m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
refreshRules();
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
connect( connect(
ProjectExplorer::ProjectManager::instance(), ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded, &ProjectExplorer::ProjectManager::projectAdded,
@@ -286,24 +278,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::openFilesChanged); &ChatRootView::openFilesChanged);
connect(
&Settings::chatAssistantSettings().enableChatTools,
&Utils::BaseAspect::changed,
this,
&ChatRootView::useToolsChanged);
connect(
&Settings::chatAssistantSettings().enableThinkingMode,
&Utils::BaseAspect::changed,
this,
&ChatRootView::useThinkingChanged);
connect(
&Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isThinkingSupportChanged);
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) { connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
m_lastErrorMessage = error; m_lastErrorMessage = error;
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
@@ -324,7 +298,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) { if (m_pendingSend.active) {
PendingSend p = m_pendingSend; PendingSend p = m_pendingSend;
m_pendingSend = {}; m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking); dispatchSend(p.message, p.attachments, p.linkedFiles);
} }
}); });
@@ -337,7 +311,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) { if (m_pendingSend.active) {
PendingSend p = m_pendingSend; PendingSend p = m_pendingSend;
m_pendingSend = {}; m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking); dispatchSend(p.message, p.attachments, p.linkedFiles);
} }
}); });
} }
@@ -373,6 +347,57 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
return m_skillsManager; return m_skillsManager;
} }
AgentFactory *ChatRootView::agentFactory() const
{
if (!m_agentFactory) {
if (auto *engine = qmlEngine(this)) {
m_agentFactory = qobject_cast<AgentFactory *>(
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
}
}
return m_agentFactory;
}
SessionManager *ChatRootView::sessionManager() const
{
if (!m_sessionManager) {
if (auto *engine = qmlEngine(this)) {
m_sessionManager = qobject_cast<SessionManager *>(
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
}
}
return m_sessionManager;
}
QodeAssist::Session *ChatRootView::ownerSession()
{
if (!m_clientInterface)
return nullptr;
m_clientInterface->setSessionManager(sessionManager());
m_clientInterface->ensureSession();
return m_clientInterface->session();
}
void ChatRootView::loadAvailableChatAgents()
{
m_agentController->setAgentFactory(agentFactory());
}
QStringList ChatRootView::availableChatAgents() const
{
return m_agentController->availableAgents();
}
QString ChatRootView::currentChatAgent() const
{
return m_agentController->currentAgent();
}
void ChatRootView::setCurrentChatAgent(const QString &name)
{
m_agentController->setCurrentAgent(name);
}
QVariantList ChatRootView::searchSkills(const QString &query) const QVariantList ChatRootView::searchSkills(const QString &query) const
{ {
QVariantList results; QVariantList results;
@@ -380,7 +405,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
if (!manager || !Settings::skillsSettings().enableSkills()) if (!manager || !Settings::skillsSettings().enableSkills())
return results; return results;
auto *project = PluginLLMCore::RulesLoader::getActiveProject(); auto *project = ProjectExplorer::ProjectManager::startupProject();
QStringList projectSkillDirs; QStringList projectSkillDirs;
if (project) { if (project) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
@@ -414,23 +439,22 @@ ChatModel *ChatRootView::chatModel() const
void ChatRootView::sendMessage(const QString &message) void ChatRootView::sendMessage(const QString &message)
{ {
const QStringList attachments = m_attachmentFiles; if (message.trimmed().isEmpty() && m_attachmentFiles.isEmpty())
const QStringList linkedFiles = m_linkedFiles; return;
const bool tools = useTools(); if (m_chatCompressor->isCompressing())
const bool thinking = useThinking();
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking))
return; return;
dispatchSend(message, attachments, linkedFiles, tools, thinking); const QStringList attachments = m_attachmentFiles;
const QStringList linkedFiles = m_linkedFiles;
if (deferSendForAutoCompress(message, attachments, linkedFiles))
return;
dispatchSend(message, attachments, linkedFiles);
} }
bool ChatRootView::deferSendForAutoCompress( bool ChatRootView::deferSendForAutoCompress(
const QString &message, const QString &message, const QStringList &attachments, const QStringList &linkedFiles)
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
if (!settings.autoCompress()) if (!settings.autoCompress())
@@ -441,8 +465,14 @@ bool ChatRootView::deferSendForAutoCompress(
if (inputTokens < threshold) if (inputTokens < threshold)
return false; return false;
if (configuredCompressionAgent().isEmpty())
return false;
if (m_recentFilePath.isEmpty()) { if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments); QString filePath = getAutosaveFilePath(message, attachments);
if (auto registry = sessionFileRegistry()) {
filePath = registry->uniqueFreePath(filePath);
}
if (filePath.isEmpty()) if (filePath.isEmpty())
return false; return false;
setRecentFilePath(filePath); setRecentFilePath(filePath);
@@ -456,17 +486,13 @@ bool ChatRootView::deferSendForAutoCompress(
.arg(inputTokens) .arg(inputTokens)
.arg(threshold)); .arg(threshold));
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true}; m_pendingSend = {message, attachments, linkedFiles, true};
compressCurrentChat(); compressCurrentChat();
return true; return true;
} }
void ChatRootView::dispatchSend( void ChatRootView::dispatchSend(
const QString &message, const QString &message, const QStringList &attachments, const QStringList &linkedFiles)
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
{ {
if (m_recentFilePath.isEmpty()) { if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments); QString filePath = getAutosaveFilePath(message, attachments);
@@ -479,14 +505,16 @@ void ChatRootView::dispatchSend(
} }
} }
m_tokenCounter->recordSent(); if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
m_clientInterface->setSkillsManager(skillsManager()); m_clientInterface->setSkillsManager(skillsManager());
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg); m_clientInterface->setSessionManager(sessionManager());
m_clientInterface->setActiveAgent(currentChatAgent());
m_clientInterface->sendMessage(message, attachments, linkedFiles);
m_fileManager->clearIntermediateStorage(); m_fileManager->clearIntermediateStorage();
clearAttachmentFiles(); clearAttachmentFiles();
setRequestProgressStatus(true);
} }
void ChatRootView::copyToClipboard(const QString &text) void ChatRootView::copyToClipboard(const QString &text)
@@ -527,22 +555,15 @@ void ChatRootView::clearMessages()
clearLinkedFiles(); clearLinkedFiles();
} }
QString ChatRootView::currentTemplate() const
{
auto &settings = Settings::generalSettings();
return settings.caModel();
}
void ChatRootView::saveHistory(const QString &filePath) void ChatRootView::saveHistory(const QString &filePath)
{ {
if (filePath != m_recentFilePath) { if (auto registry = sessionFileRegistry();
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) { registry && registry->isLockedByOther(filePath, ownerSession())) {
m_lastErrorMessage m_lastErrorMessage = tr(
= tr("This chat file is already in use by another QodeAssist chat session."); "This chat file is already in use by another QodeAssist chat session.");
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
return; return;
} }
}
auto result = m_historyStore->save(filePath); auto result = m_historyStore->save(filePath);
if (!result.success) { if (!result.success) {
@@ -554,19 +575,24 @@ void ChatRootView::saveHistory(const QString &filePath)
void ChatRootView::loadHistory(const QString &filePath) void ChatRootView::loadHistory(const QString &filePath)
{ {
if (filePath != m_recentFilePath) { if (auto registry = sessionFileRegistry();
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) { registry && registry->isLockedByOther(filePath, ownerSession())) {
m_lastErrorMessage m_lastErrorMessage = tr("This chat is already open in another QodeAssist chat session.");
= tr("This chat is already open in another QodeAssist chat session.");
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
return; return;
} }
}
m_clientInterface->cancelRequest();
auto result = m_historyStore->load(filePath); auto result = m_historyStore->load(filePath);
if (!result.success) { if (!result.success) {
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage)); LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
} else { } else {
if (!result.errorMessage.isEmpty()) {
m_lastInfoMessage = result.errorMessage;
emit lastInfoMessageChanged();
LOG_MESSAGE(QString("Chat history loaded with issues: %1").arg(result.errorMessage));
}
setRecentFilePath(filePath); setRecentFilePath(filePath);
} }
@@ -586,6 +612,12 @@ void ChatRootView::showSaveDialog()
m_historyStore->showSaveDialog(); m_historyStore->showSaveDialog();
} }
void ChatRootView::resetChatTo(int index)
{
m_clientInterface->cancelRequest();
m_chatModel->resetModelTo(index);
}
void ChatRootView::showLoadDialog() void ChatRootView::showLoadDialog()
{ {
m_historyStore->showLoadDialog(); m_historyStore->showLoadDialog();
@@ -821,25 +853,6 @@ void ChatRootView::openChatHistoryFolder()
m_historyStore->openHistoryFolder(); m_historyStore->openHistoryFolder();
} }
void ChatRootView::openRulesFolder()
{
auto project = ProjectExplorer::ProjectManager::startupProject();
if (!project) {
return;
}
QString projectPath = project->projectDirectory().toFSPathString();
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
QDir dir(rulesPath);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
void ChatRootView::openSettings() void ChatRootView::openSettings()
{ {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
@@ -890,13 +903,12 @@ QString ChatRootView::chatTitle() const
QString ChatRootView::computeChatTitle() const QString ChatRootView::computeChatTitle() const
{ {
if (!m_chatModel) if (!m_history)
return {}; return {};
const auto history = m_chatModel->getChatHistory(); for (const auto &msg : m_history->messages()) {
for (const auto &msg : history) { if (msg.role() != Message::Role::User)
if (msg.role != ChatModel::User)
continue; continue;
const QString content = msg.content.trimmed(); const QString content = msg.text().trimmed();
if (content.isEmpty()) if (content.isEmpty())
continue; continue;
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed(); const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
@@ -954,8 +966,6 @@ void ChatRootView::relocateToWindow()
clearAttachmentFiles(); clearAttachmentFiles();
emit closeHostRequested(); emit closeHostRequested();
// Closing the source split raises the main window; re-raise the chat window once that
// queued teardown has run. The registry outlives this view, which the split close deletes.
if (auto registry = sessionFileRegistry()) { if (auto registry = sessionFileRegistry()) {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
registry, registry,
@@ -1052,7 +1062,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
registry->release(m_recentFilePath); registry->release(m_recentFilePath);
} }
if (!filePath.isEmpty()) { if (!filePath.isEmpty()) {
registry->lock(filePath); registry->lock(filePath, ownerSession());
} }
} }
@@ -1064,11 +1074,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath) bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{ {
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath); if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString())); .arg(filePath.toFSPathString()));
return true; return true;
@@ -1120,71 +1126,9 @@ QString ChatRootView::lastErrorMessage() const
return m_lastErrorMessage; return m_lastErrorMessage;
} }
QVariantList ChatRootView::activeRules() const
{
return m_activeRules;
}
int ChatRootView::activeRulesCount() const
{
return m_activeRules.size();
}
QString ChatRootView::getRuleContent(int index)
{
if (index < 0 || index >= m_activeRules.size())
return QString();
return PluginLLMCore::RulesLoader::loadRuleFileContent(
m_activeRules[index].toMap()["filePath"].toString());
}
void ChatRootView::refreshRules()
{
m_activeRules.clear();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (!project) {
emit activeRulesChanged();
emit activeRulesCountChanged();
return;
}
auto ruleFiles
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
for (const auto &ruleFile : ruleFiles) {
QVariantMap ruleMap;
ruleMap["filePath"] = ruleFile.filePath;
ruleMap["fileName"] = ruleFile.fileName;
ruleMap["category"] = ruleFile.category;
m_activeRules.append(ruleMap);
}
emit activeRulesChanged();
emit activeRulesCountChanged();
}
bool ChatRootView::useTools() const bool ChatRootView::useTools() const
{ {
return Settings::chatAssistantSettings().enableChatTools(); return m_agentController->currentSupportsTools();
}
void ChatRootView::setUseTools(bool enabled)
{
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
}
bool ChatRootView::useThinking() const
{
return Settings::chatAssistantSettings().enableThinkingMode();
}
void ChatRootView::setUseThinking(bool enabled)
{
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
} }
void ChatRootView::applyFileEdit(const QString &editId) void ChatRootView::applyFileEdit(const QString &editId)
@@ -1217,11 +1161,6 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
m_fileEditController->undoAllForCurrentMessage(); m_fileEditController->undoAllForCurrentMessage();
} }
void ChatRootView::updateCurrentMessageEditsStats()
{
m_fileEditController->updateStats();
}
int ChatRootView::currentMessageTotalEdits() const int ChatRootView::currentMessageTotalEdits() const
{ {
return m_fileEditController->totalEdits(); return m_fileEditController->totalEdits();
@@ -1247,14 +1186,6 @@ QString ChatRootView::lastInfoMessage() const
return m_lastInfoMessage; return m_lastInfoMessage;
} }
bool ChatRootView::isThinkingSupport() const
{
auto providerName = Settings::generalSettings().caProvider();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
}
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
{ {
for (const QString &filePath : attachments) { for (const QString &filePath : attachments) {
@@ -1273,64 +1204,17 @@ bool ChatRootView::isImageFile(const QString &filePath) const
return imageExtensions.contains(fileInfo.suffix().toLower()); return imageExtensions.contains(fileInfo.suffix().toLower());
} }
void ChatRootView::loadAvailableConfigurations() QString ChatRootView::configuredCompressionAgent() const
{ {
m_configurationController->loadAvailableConfigurations(); const QString configured = Settings::PipelinesConfig::load().rosters.chatCompression;
if (!configured.isEmpty() && agentFactory() && agentFactory()->configByName(configured))
return configured;
return {};
} }
void ChatRootView::applyConfiguration(const QString &configName) bool ChatRootView::canCompress() const
{ {
m_configurationController->applyConfiguration(configName); return !configuredCompressionAgent().isEmpty();
}
QStringList ChatRootView::availableConfigurations() const
{
return m_configurationController->availableConfigurations();
}
QString ChatRootView::currentConfiguration() const
{
return m_configurationController->currentConfiguration();
}
void ChatRootView::loadAvailableAgentRoles()
{
m_agentRoleController->loadAvailableRoles();
}
void ChatRootView::applyAgentRole(const QString &roleName)
{
m_agentRoleController->applyRole(roleName);
}
QStringList ChatRootView::availableAgentRoles() const
{
return m_agentRoleController->availableRoles();
}
QString ChatRootView::currentAgentRole() const
{
return m_agentRoleController->currentRole();
}
QString ChatRootView::baseSystemPrompt() const
{
return m_agentRoleController->baseSystemPrompt();
}
QString ChatRootView::currentAgentRoleDescription() const
{
return m_agentRoleController->currentRoleDescription();
}
QString ChatRootView::currentAgentRoleSystemPrompt() const
{
return m_agentRoleController->currentRoleSystemPrompt();
}
void ChatRootView::openAgentRolesSettings()
{
m_agentRoleController->openSettings();
} }
void ChatRootView::compressCurrentChat() void ChatRootView::compressCurrentChat()
@@ -1347,9 +1231,21 @@ void ChatRootView::compressCurrentChat()
return; return;
} }
autosave(); const QString compressionAgent = configuredCompressionAgent();
if (compressionAgent.isEmpty())
return;
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel); const auto saveResult = m_historyStore->save(m_recentFilePath);
if (!saveResult.success) {
m_lastErrorMessage
= tr("Failed to save chat before compression: %1").arg(saveResult.errorMessage);
emit lastErrorMessageChanged();
return;
}
m_chatCompressor->setSessionManager(sessionManager());
m_chatCompressor->setActiveAgent(compressionAgent);
m_chatCompressor->startCompression(m_recentFilePath, m_history);
} }
void ChatRootView::cancelCompression() void ChatRootView::cancelCompression()

View File

@@ -11,18 +11,23 @@
#include "ChatFileManager.hpp" #include "ChatFileManager.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
namespace QodeAssist {
class AgentFactory;
class SessionManager;
class ConversationHistory;
class Session;
} // namespace QodeAssist
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor; class ChatCompressor;
class AgentRoleController; class ChatAgentController;
class ChatConfigurationController;
class FileEditController; class FileEditController;
class InputTokenCounter; class InputTokenCounter;
class ChatHistoryStore; class ChatHistoryStore;
@@ -32,7 +37,6 @@ class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL) Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL) Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL) Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
@@ -46,25 +50,21 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL) Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL) Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL) Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged 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) Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL) Q_PROPERTY(
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL) QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL) currentChatAgentChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool canCompress READ canCompress NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL) Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL) Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
@@ -75,13 +75,13 @@ public:
~ChatRootView() override; ~ChatRootView() override;
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const;
void saveHistory(const QString &filePath); void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath); void loadHistory(const QString &filePath);
Q_INVOKABLE void showSaveDialog(); Q_INVOKABLE void showSaveDialog();
Q_INVOKABLE void showLoadDialog(); Q_INVOKABLE void showLoadDialog();
Q_INVOKABLE void resetChatTo(int index);
void autosave(); void autosave();
QString getAutosaveFilePath() const; QString getAutosaveFilePath() const;
@@ -104,7 +104,6 @@ public:
QString sendShortcutText() const; QString sendShortcutText() const;
Q_INVOKABLE void setIsSyncOpenFiles(bool state); Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder(); Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings(); Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath); Q_INVOKABLE void openFileInEditor(const QString &filePath);
@@ -140,17 +139,9 @@ public:
QString lastErrorMessage() const; QString lastErrorMessage() const;
QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
Q_INVOKABLE QVariantList searchSkills(const QString &query) const; Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
bool useTools() const; bool useTools() const;
void setUseTools(bool enabled);
bool useThinking() const;
void setUseThinking(bool enabled);
Q_INVOKABLE void applyFileEdit(const QString &editId); Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId); Q_INVOKABLE void rejectFileEdit(const QString &editId);
@@ -159,24 +150,14 @@ public:
Q_INVOKABLE void applyAllFileEditsForCurrentMessage(); Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); 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 compressCurrentChat();
Q_INVOKABLE void cancelCompression(); Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles(); Q_INVOKABLE void loadAvailableChatAgents();
Q_INVOKABLE void applyAgentRole(const QString &roleId); QStringList availableChatAgents() const;
Q_INVOKABLE void openAgentRolesSettings(); QString currentChatAgent() const;
QStringList availableAgentRoles() const; void setCurrentChatAgent(const QString &name);
QString currentAgentRole() const;
QString baseSystemPrompt() const;
QString currentAgentRoleDescription() const;
QString currentAgentRoleSystemPrompt() const;
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
@@ -185,9 +166,8 @@ public:
QString lastInfoMessage() const; QString lastInfoMessage() const;
bool isThinkingSupport() const;
bool isCompressing() const; bool isCompressing() const;
bool canCompress() const;
bool isInEditor() const; bool isInEditor() const;
void setInEditor(bool value); void setInEditor(bool value);
@@ -206,7 +186,6 @@ public slots:
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged(); void attachmentFilesChanged();
void linkedFilesChanged(); void linkedFilesChanged();
void inputTokensCountChanged(); void inputTokensCountChanged();
@@ -223,20 +202,12 @@ signals:
void lastErrorMessageChanged(); void lastErrorMessageChanged();
void lastInfoMessageChanged(); void lastInfoMessageChanged();
void sendShortcutTextChanged(); void sendShortcutTextChanged();
void activeRulesChanged();
void activeRulesCountChanged();
void useToolsChanged(); void useToolsChanged();
void useThinkingChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged(); void availableChatAgentsChanged();
void availableConfigurationsChanged(); void currentChatAgentChanged();
void currentConfigurationChanged();
void availableAgentRolesChanged();
void currentAgentRoleChanged();
void baseSystemPromptChanged();
void isCompressingChanged(); void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
@@ -254,27 +225,22 @@ private:
void triggerOpenChatCommand(Utils::Id commandId); void triggerOpenChatCommand(Utils::Id commandId);
void handOffSession(); void handOffSession();
bool deferSendForAutoCompress( bool deferSendForAutoCompress(
const QString &message, const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
void dispatchSend( void dispatchSend(
const QString &message, const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
const QStringList &attachments, QString configuredCompressionAgent() const;
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
bool hasImageAttachments(const QStringList &attachments) const; bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const; SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const; Skills::SkillsManager *skillsManager() const;
AgentFactory *agentFactory() const;
SessionManager *sessionManager() const;
QodeAssist::Session *ownerSession();
QodeAssist::ConversationHistory *m_history;
ChatModel *m_chatModel; ChatModel *m_chatModel;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager; ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath; QString m_recentFilePath;
QStringList m_attachmentFiles; QStringList m_attachmentFiles;
QStringList m_linkedFiles; QStringList m_linkedFiles;
@@ -283,8 +249,6 @@ private:
QString message; QString message;
QStringList attachments; QStringList attachments;
QStringList linkedFiles; QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false; bool active = false;
}; };
PendingSend m_pendingSend; PendingSend m_pendingSend;
@@ -294,13 +258,11 @@ private:
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_lastInfoMessage; QString m_lastInfoMessage;
ChatCompressor *m_chatCompressor; ChatCompressor *m_chatCompressor;
AgentRoleController *m_agentRoleController; ChatAgentController *m_agentController;
ChatConfigurationController *m_configurationController;
FileEditController *m_fileEditController; FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter; InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore; ChatHistoryStore *m_historyStore;
@@ -308,6 +270,8 @@ private:
mutable bool m_sessionFileRegistryResolved = false; mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager; mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false; mutable bool m_skillsManagerResolved = false;
mutable QPointer<AgentFactory> m_agentFactory;
mutable QPointer<SessionManager> m_sessionManager;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -5,7 +5,8 @@
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include <QBuffer> #include <memory>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -13,12 +14,69 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <LLMQore/ContentBlocks.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <MessageSerializer.hpp>
#include <PluginBlocks.hpp>
#include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
const QString ChatSerializer::VERSION = "0.2"; namespace {
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath) const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
void registerEditFromResult(const QString &result)
{ {
const int pos = result.indexOf(kFileEditMarker);
if (pos < 0)
return;
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (!doc.isObject())
return;
const QJsonObject obj = doc.object();
const QString editId = obj.value("edit_id").toString();
const QString filePath = obj.value("file").toString();
if (editId.isEmpty() || filePath.isEmpty())
return;
Context::ChangesManager::instance().addFileEdit(
editId,
filePath,
obj.value("old_content").toString(),
obj.value("new_content").toString(),
false,
true);
}
void appendMergingAssistants(std::vector<Message> &out, Message message)
{
if (message.role() == Message::Role::Assistant && !out.empty()
&& out.back().role() == Message::Role::Assistant) {
if (out.back().id().isEmpty() && !message.id().isEmpty())
out.back().setId(message.id());
for (auto &block : message.takeBlocks())
out.back().appendBlock(std::move(block));
return;
}
out.push_back(std::move(message));
}
} // namespace
const QString ChatSerializer::VERSION = "0.3";
SerializationResult ChatSerializer::saveToFile(
const ConversationHistory *history, const QString &filePath, const QJsonObject &usage)
{
if (!history)
return {false, "No conversation history"};
if (!ensureDirectoryExists(filePath)) { if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"}; return {false, "Failed to create directory structure"};
} }
@@ -28,9 +86,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {false, QString("Failed to open file for writing: %1").arg(filePath)}; return {false, QString("Failed to open file for writing: %1").arg(filePath)};
} }
QJsonObject root = serializeChat(model, filePath); QJsonDocument doc(serializeChat(history, usage));
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) { if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())}; return {false, QString("Failed to write to file: %1").arg(file.errorString())};
} }
@@ -38,8 +94,12 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {true, QString()}; return {true, QString()};
} }
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath) SerializationResult ChatSerializer::loadFromFile(
ConversationHistory *history, const QString &filePath, QJsonObject *usageOut)
{ {
if (!history)
return {false, "No conversation history"};
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)}; return {false, QString("Failed to open file for reading: %1").arg(filePath)};
@@ -51,180 +111,182 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
return {false, QString("JSON parse error: %1").arg(error.errorString())}; return {false, QString("JSON parse error: %1").arg(error.errorString())};
} }
QJsonObject root = doc.object(); const QJsonObject root = doc.object();
QString version = root["version"].toString(); const QString version = root["version"].toString();
if (!validateVersion(version)) { if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)}; return {false, QString("Unsupported version: %1").arg(version)};
} }
if (!deserializeChat(model, root, filePath)) { if (version == VERSION)
return {false, "Failed to deserialize chat data"}; return loadCurrent(history, root, usageOut);
} return loadLegacy(history, root, usageOut);
return {true, QString()};
} }
QJsonObject ChatSerializer::serializeMessage( QJsonObject ChatSerializer::serializeChat(
const ChatModel::Message &message, const QString &chatFilePath) const ConversationHistory *history, const QJsonObject &usage)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
if (message.isRedacted) {
messageObj["isRedacted"] = true;
}
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
if (message.role == ChatModel::ChatRole::Tool) {
if (!message.toolName.isEmpty())
messageObj["toolName"] = message.toolName;
if (!message.toolArguments.isEmpty())
messageObj["toolArguments"] = message.toolArguments;
if (!message.toolResult.isEmpty())
messageObj["toolResult"] = message.toolResult;
}
if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) {
QJsonObject attachmentObj;
attachmentObj["fileName"] = attachment.filename;
attachmentObj["storedPath"] = attachment.content;
attachmentsArray.append(attachmentObj);
}
messageObj["attachments"] = attachmentsArray;
}
if (!message.images.isEmpty()) {
QJsonArray imagesArray;
for (const auto &image : message.images) {
QJsonObject imageObj;
imageObj["fileName"] = image.fileName;
imageObj["storedPath"] = image.storedPath;
imageObj["mediaType"] = image.mediaType;
imagesArray.append(imageObj);
}
messageObj["images"] = imagesArray;
}
if (message.promptTokens > 0 || message.completionTokens > 0) {
QJsonObject usageObj;
usageObj["promptTokens"] = message.promptTokens;
usageObj["completionTokens"] = message.completionTokens;
if (message.cachedPromptTokens > 0)
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
if (message.reasoningTokens > 0)
usageObj["reasoningTokens"] = message.reasoningTokens;
messageObj["usage"] = usageObj;
}
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(
const QJsonObject &json, const QString &chatFilePath)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
message.toolName = json["toolName"].toString();
message.toolArguments = json["toolArguments"].toObject();
message.toolResult = json["toolResult"].toString();
if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray();
for (const auto &attachmentValue : attachmentsArray) {
QJsonObject attachmentObj = attachmentValue.toObject();
Context::ContentFile attachment;
attachment.filename = attachmentObj["fileName"].toString();
attachment.content = attachmentObj["storedPath"].toString();
message.attachments.append(attachment);
}
}
if (json.contains("images")) {
QJsonArray imagesArray = json["images"].toArray();
for (const auto &imageValue : imagesArray) {
QJsonObject imageObj = imageValue.toObject();
ChatModel::ImageAttachment image;
image.fileName = imageObj["fileName"].toString();
image.storedPath = imageObj["storedPath"].toString();
image.mediaType = imageObj["mediaType"].toString();
message.images.append(image);
}
}
if (json.contains("usage")) {
const QJsonObject usageObj = json["usage"].toObject();
message.promptTokens = usageObj["promptTokens"].toInt();
message.completionTokens = usageObj["completionTokens"].toInt();
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
}
return message;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
{ {
QJsonArray messagesArray; QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) { for (const auto &message : history->messages())
messagesArray.append(serializeMessage(message, chatFilePath)); messagesArray.append(MessageSerializer::toJson(message));
}
QJsonObject root; QJsonObject root;
root["version"] = VERSION; root["version"] = VERSION;
root["messages"] = messagesArray; root["messages"] = messagesArray;
if (!usage.isEmpty())
root["usage"] = usage;
return root; return root;
} }
bool ChatSerializer::deserializeChat( SerializationResult ChatSerializer::loadCurrent(
ChatModel *model, const QJsonObject &json, const QString &chatFilePath) ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut)
{ {
QJsonArray messagesArray = json["messages"].toArray(); history->clear();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
for (const auto &messageValue : messagesArray) { int skipped = 0;
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath)); const QJsonArray messagesArray = root["messages"].toArray();
for (const auto &value : messagesArray) {
bool ok = false;
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
if (ok)
history->append(std::move(message));
else
++skipped;
} }
model->clear(); if (usageOut)
*usageOut = root["usage"].toObject();
model->setLoadingFromHistory(true); registerHistoricalFileEdits(history);
if (skipped > 0) {
for (const auto &message : messages) { return {true, QString("%1 message(s) could not be parsed and were skipped").arg(skipped)};
model->addMessage(
message.content,
message.role,
message.id,
message.attachments,
message.images,
message.isRedacted,
message.signature);
if (message.role == ChatModel::ChatRole::Tool) {
model->setToolMessageData(
message.id, message.toolName, message.toolArguments, message.toolResult);
} }
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3") return {true, QString()};
.arg(message.images.size()) }
.arg(message.isRedacted)
.arg(message.signature.length())); SerializationResult ChatSerializer::loadLegacy(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut)
{
history->clear();
QJsonObject usage;
const auto collectUsage = [&usage](const QJsonObject &mj) {
const QString id = mj["id"].toString();
const QJsonObject legacyUsage = mj["usage"].toObject();
if (id.isEmpty() || legacyUsage.isEmpty())
return;
QJsonObject entry;
entry["prompt"] = legacyUsage["promptTokens"].toInt();
entry["completion"] = legacyUsage["completionTokens"].toInt();
entry["cached"] = legacyUsage["cachedPromptTokens"].toInt();
entry["reasoning"] = legacyUsage["reasoningTokens"].toInt();
usage.insert(id, entry);
};
std::vector<Message> merged;
const QJsonArray arr = root["messages"].toArray();
int i = 0;
while (i < arr.size()) {
const QJsonObject mj = arr[i].toObject();
const auto role = static_cast<LegacyRole>(mj["role"].toInt());
if (role == LegacyRole::Tool) {
Message assistant(Message::Role::Assistant);
Message toolResults(Message::Role::User);
while (i < arr.size()
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt())
== LegacyRole::Tool) {
const QJsonObject tj = arr[i].toObject();
const QString toolName = tj["toolName"].toString();
const QString id = tj["id"].toString();
if (!toolName.isEmpty()) {
assistant.appendBlock(
std::make_unique<LLMQore::ToolUseContent>(
id, toolName, tj["toolArguments"].toObject()));
toolResults.appendBlock(
std::make_unique<LLMQore::ToolResultContent>(id, tj["toolResult"].toString()));
}
++i;
}
if (!assistant.blocks().empty()) {
appendMergingAssistants(merged, std::move(assistant));
merged.push_back(std::move(toolResults));
}
continue;
} }
model->setLoadingFromHistory(false); ++i;
return true; if (role == LegacyRole::FileEdit)
continue;
if (role == LegacyRole::Thinking) {
const QString content = mj["content"].toString();
const QString signature = mj["signature"].toString();
Message assistant(Message::Role::Assistant);
if (mj["isRedacted"].toBool(false)) {
assistant.appendBlock(std::make_unique<LLMQore::RedactedThinkingContent>(signature));
} else {
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
assistant.appendBlock(
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
}
appendMergingAssistants(merged, std::move(assistant));
continue;
}
if (role == LegacyRole::User) {
Message user(Message::Role::User, mj["id"].toString());
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
for (const auto &a : mj["attachments"].toArray()) {
const QJsonObject ao = a.toObject();
user.appendBlock(
std::make_unique<StoredAttachmentContent>(
ao["fileName"].toString(), ao["storedPath"].toString()));
}
for (const auto &im : mj["images"].toArray()) {
const QJsonObject io = im.toObject();
user.appendBlock(
std::make_unique<StoredImageContent>(
io["fileName"].toString(),
io["storedPath"].toString(),
io["mediaType"].toString()));
}
merged.push_back(std::move(user));
} else {
const QString content = mj["content"].toString();
if (content.trimmed().isEmpty())
continue;
const Message::Role mapped = role == LegacyRole::System ? Message::Role::System
: Message::Role::Assistant;
Message message(mapped, mj["id"].toString());
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
collectUsage(mj);
if (mapped == Message::Role::Assistant)
appendMergingAssistants(merged, std::move(message));
else
merged.push_back(std::move(message));
}
}
for (auto &message : merged)
history->append(std::move(message));
if (usageOut)
*usageOut = usage;
registerHistoricalFileEdits(history);
return {true, QString()};
}
void ChatSerializer::registerHistoricalFileEdits(const ConversationHistory *history)
{
for (const auto &message : history->messages()) {
for (const auto &block : message.blocks()) {
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
registerEditFromResult(tr->result());
}
}
} }
bool ChatSerializer::ensureDirectoryExists(const QString &filePath) bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
@@ -236,18 +298,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
bool ChatSerializer::validateVersion(const QString &version) bool ChatSerializer::validateVersion(const QString &version)
{ {
if (version == VERSION) { return version == VERSION || version == "0.2" || version == "0.1";
return true;
}
if (version == "0.1") {
LOG_MESSAGE(
"Loading chat from old format 0.1 - images folder structure has changed from _images "
"to _content");
return true;
}
return false;
} }
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath) QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
@@ -303,10 +354,20 @@ bool ChatSerializer::saveContentToStorage(
return true; return true;
} }
QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath) QString ChatSerializer::loadContentFromStorage(
const QString &chatFilePath, const QString &storedPath, StoredContentCache *cache)
{ {
QString contentFolder = getChatContentFolder(chatFilePath); const QString contentFolder = getChatContentFolder(chatFilePath);
QString fullPath = QDir(contentFolder).filePath(storedPath); const QString fullPath = QDir(contentFolder).filePath(storedPath);
const QFileInfo info(fullPath);
if (cache) {
const auto it = cache->constFind(fullPath);
if (it != cache->constEnd() && it->modified == info.lastModified()
&& it->size == info.size()) {
return it->base64;
}
}
QFile file(fullPath); QFile file(fullPath);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
@@ -314,10 +375,12 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
return QString(); return QString();
} }
QByteArray contentData = file.readAll(); const QString base64 = QString::fromUtf8(file.readAll().toBase64());
file.close();
return contentData.toBase64(); if (cache)
cache->insert(fullPath, {info.lastModified(), info.size(), base64});
return base64;
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -4,11 +4,14 @@
#pragma once #pragma once
#include <QJsonArray> #include <QDateTime>
#include <QHash>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include "ChatModel.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -18,29 +21,41 @@ struct SerializationResult
QString errorMessage; QString errorMessage;
}; };
struct StoredContentEntry
{
QDateTime modified;
qint64 size = 0;
QString base64;
};
using StoredContentCache = QHash<QString, StoredContentEntry>;
class ChatSerializer class ChatSerializer
{ {
public: public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath); static const QString VERSION;
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
// Public for testing purposes static SerializationResult saveToFile(
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath); const ConversationHistory *history, const QString &filePath, const QJsonObject &usage = {});
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath); static SerializationResult loadFromFile(
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath); ConversationHistory *history, const QString &filePath, QJsonObject *usageOut = nullptr);
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
// Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath); static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage(const QString &chatFilePath, static bool saveContentToStorage(
const QString &chatFilePath,
const QString &fileName, const QString &fileName,
const QString &base64Data, const QString &base64Data,
QString &storedPath); QString &storedPath);
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath); static QString loadContentFromStorage(
const QString &chatFilePath, const QString &storedPath, StoredContentCache *cache = nullptr);
private: private:
static const QString VERSION; static QJsonObject serializeChat(const ConversationHistory *history, const QJsonObject &usage);
static constexpr int CURRENT_VERSION = 1; static SerializationResult loadCurrent(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
static SerializationResult loadLegacy(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
static void registerHistoricalFileEdits(const ConversationHistory *history);
static bool ensureDirectoryExists(const QString &filePath); static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version); static bool validateVersion(const QString &version);

File diff suppressed because it is too large Load Diff

View File

@@ -5,16 +5,25 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QSet> #include <QPointer>
#include <QString> #include <QString>
#include <QVector> #include <QStringList>
#include <memory>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Provider.hpp" #include "ChatSerializer.hpp"
#include "pluginllmcore/IPromptProvider.hpp" #include <ErrorInfo.hpp>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <ResponseEvent.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
} // namespace QodeAssist
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
@@ -26,18 +35,18 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager); void setSkillsManager(Skills::SkillsManager *skillsManager);
void setSessionManager(SessionManager *sessionManager);
void setHistory(ConversationHistory *history);
void setActiveAgent(const QString &agentName);
void sendMessage( void sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments = {}, const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {}, const QList<QString> &linkedFiles = {});
bool useTools = false,
bool useThinking = false);
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
@@ -46,57 +55,50 @@ public:
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
void ensureSession();
Session *session() const;
signals: signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely(); void messageReceivedCompletely();
void requestStarted(const QString &requestId); void requestStarted(const QString &requestId);
void requestCancelled();
void messageUsageReceived( void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens); int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
private slots:
void handlePartialResponse(const QString &requestId, const QString &partialText);
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);
void handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void handleToolExecutionStarted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments);
void handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput);
private: private:
void handleLLMResponse(const QString &response, const QJsonObject &request); bool ensureAgentBound();
QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles( void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
const QString &basePrompt, const QList<QString> &linkedFiles) const; void onSessionFinished(const QString &requestId);
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
void onSessionCancelled(const QString &requestId);
QStringList invokedSkillNames(const QString &message) const;
QString buildChatContextLayer() const;
QString requestIdForSession(Session *session) const;
bool isImageFile(const QString &filePath) const; bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
bool dropPreToolText = false;
}; };
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QPointer<ConversationHistory> m_history;
Skills::SkillsManager *m_skillsManager = nullptr; Skills::SkillsManager *m_skillsManager = nullptr;
QPointer<SessionManager> m_sessionManager;
QPointer<Session> m_session;
QString m_activeAgent;
QString m_boundAgent;
QString m_chatFilePath; QString m_chatFilePath;
std::shared_ptr<StoredContentCache> m_contentCache;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
QSet<QString> m_awaitingContinuation;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -10,15 +10,13 @@
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include "ChatModel.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "context/ChangesManager.h" #include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent) FileEditController::FileEditController(QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel)
{ {
auto &changes = Context::ChangesManager::instance(); auto &changes = Context::ChangesManager::instance();
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) { connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
@@ -80,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId)); LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) { if (Context::ChangesManager::instance().applyFileEdit(editId)) {
emit infoMessage(QString("File edit applied successfully")); emit infoMessage(QString("File edit applied successfully"));
updateFileEditStatus(editId, "applied");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -95,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId)); LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) { if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
emit infoMessage(QString("File edit rejected")); emit infoMessage(QString("File edit rejected"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -110,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId)); LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) { if (Context::ChangesManager::instance().undoFileEdit(editId)) {
emit infoMessage(QString("File edit undone successfully")); emit infoMessage(QString("File edit undone successfully"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -163,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath)); LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
} }
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
{
auto messages = m_chatModel->getChatHistory();
for (int i = 0; i < messages.size(); ++i) {
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
QString content = messages[i].content;
const QString marker = "QODEASSIST_FILE_EDIT:";
int markerPos = content.indexOf(marker);
QString jsonStr = content;
if (markerPos >= 0) {
jsonStr = content.mid(markerPos + marker.length());
}
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject obj = doc.object();
obj["status"] = status;
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (!edit.statusMessage.isEmpty()) {
obj["status_message"] = edit.statusMessage;
}
QString updatedContent = marker
+ QString::fromUtf8(
QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_chatModel->updateMessageContent(editId, updatedContent);
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
}
break;
}
}
updateStats();
}
void FileEditController::applyAllForCurrentMessage() void FileEditController::applyAllForCurrentMessage()
{ {
if (m_currentRequestId.isEmpty()) { if (m_currentRequestId.isEmpty()) {
@@ -223,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
: QString("Failed to apply some file edits:\n%1").arg(errorMsg)); : QString("Failed to apply some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
updateStats(); updateStats();
} }
@@ -255,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
: QString("Failed to undo some file edits:\n%1").arg(errorMsg)); : QString("Failed to undo some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
updateStats(); updateStats();
} }

View File

@@ -9,14 +9,12 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class FileEditController : public QObject class FileEditController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr); explicit FileEditController(QObject *parent = nullptr);
void setCurrentRequestId(const QString &requestId); void setCurrentRequestId(const QString &requestId);
void clearCurrentRequestId(); void clearCurrentRequestId();
@@ -41,9 +39,6 @@ signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status);
ChatModel *m_chatModel;
QString m_currentRequestId; QString m_currentRequestId;
int m_totalEdits{0}; int m_totalEdits{0};
int m_appliedEdits{0}; int m_appliedEdits{0};

View File

@@ -88,22 +88,6 @@ void FileMentionItem::moveDown()
} }
} }
void FileMentionItem::selectCurrent()
{
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
return;
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
if (item.value("isProject").toBool()) {
emit projectSelected(item.value("projectName").toString());
} else {
emit fileSelected(
item.value("absolutePath").toString(),
item.value("relativePath").toString(),
item.value("projectName").toString());
}
}
void FileMentionItem::dismiss() void FileMentionItem::dismiss()
{ {
m_searchResults.clear(); m_searchResults.clear();

View File

@@ -29,7 +29,6 @@ public:
Q_INVOKABLE void refreshSearch(); Q_INVOKABLE void refreshSearch();
Q_INVOKABLE void moveUp(); Q_INVOKABLE void moveUp();
Q_INVOKABLE void moveDown(); Q_INVOKABLE void moveDown();
Q_INVOKABLE void selectCurrent();
Q_INVOKABLE void dismiss(); Q_INVOKABLE void dismiss();
Q_INVOKABLE QVariantMap handleFileSelection( Q_INVOKABLE QVariantMap handleFileSelection(

View File

@@ -4,50 +4,20 @@
#include "InputTokenCounter.hpp" #include "InputTokenCounter.hpp"
#include <algorithm>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp"
#include "ChatModel.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
InputTokenCounter::InputTokenCounter( InputTokenCounter::InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent) ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel) , m_history(history)
, m_contextManager(contextManager) , m_contextManager(contextManager)
{ {
auto &settings = Settings::chatAssistantSettings();
connect(
&settings.useSystemPrompt,
&Utils::BaseAspect::changed,
this,
&InputTokenCounter::recompute);
connect(
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
connect(
&settings.enableChatTools,
&Utils::BaseAspect::changed,
this,
&InputTokenCounter::recompute);
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
rewireToolsChangedConnection();
recompute();
});
rewireToolsChangedConnection();
recompute(); recompute();
} }
@@ -74,33 +44,8 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
recompute(); recompute();
} }
void InputTokenCounter::rewireToolsChangedConnection()
{
if (m_toolsChangedConn)
QObject::disconnect(m_toolsChangedConn);
m_toolsChangedConn = {};
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider)
return;
auto *tm = provider->toolsManager();
if (!tm)
return;
m_toolsChangedConn = connect(
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
}
void InputTokenCounter::recompute() void InputTokenCounter::recompute()
{ {
int inputTokens = m_messageTokens;
auto &settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
}
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) { const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
int imageTokens = 0; int imageTokens = 0;
for (const QString &p : paths) { for (const QString &p : paths) {
@@ -112,15 +57,24 @@ void InputTokenCounter::recompute()
return imageTokens; return imageTokens;
}; };
int pendingTokens = m_messageTokens;
if (!m_attachments.isEmpty()) { if (!m_attachments.isEmpty()) {
QStringList textPaths; QStringList textPaths;
inputTokens += splitImageEstimate(m_attachments, textPaths); pendingTokens += splitImageEstimate(m_attachments, textPaths);
if (!textPaths.isEmpty()) { if (!textPaths.isEmpty()) {
auto attachFiles = m_contextManager->getContentFiles(textPaths); auto attachFiles = m_contextManager->getContentFiles(textPaths);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles); pendingTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
} }
} }
if (m_hasServerUsage && m_history && !m_history->isEmpty()) {
m_inputTokens = m_serverInputTokens + pendingTokens;
emit inputTokensChanged();
return;
}
int inputTokens = pendingTokens;
if (!m_linkedFiles.isEmpty()) { if (!m_linkedFiles.isEmpty()) {
QStringList textPaths; QStringList textPaths;
inputTokens += splitImageEstimate(m_linkedFiles, textPaths); inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
@@ -130,54 +84,32 @@ void InputTokenCounter::recompute()
} }
} }
const auto &history = m_chatModel->getChatHistory(); if (m_history) {
for (const auto &message : history) { for (const auto &message : m_history->messages()) {
inputTokens += Context::TokenUtils::estimateTokens(message.content); inputTokens += Context::TokenUtils::estimateTokens(message.text());
inputTokens += 4; // + role inputTokens += 4;
}
if (settings.enableChatTools()) {
const auto providerName = Settings::generalSettings().caProvider();
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
providerName)) {
if (auto *tm = provider->toolsManager()) {
const QJsonArray toolDefs = tm->getToolsDefinitions();
if (!toolDefs.isEmpty()) {
const QByteArray serialized
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
inputTokens += static_cast<int>(serialized.size() / 4);
}
}
} }
} }
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor); m_inputTokens = inputTokens;
emit inputTokensChanged(); emit inputTokensChanged();
} }
void InputTokenCounter::recordSent() void InputTokenCounter::recordServerUsage(int promptTokens, int cachedTokens)
{ {
m_lastSentEstimate = m_calibrationFactor > 0.0 const int serverInput = promptTokens + cachedTokens;
? static_cast<int>(m_inputTokens / m_calibrationFactor) if (serverInput <= 0)
: m_inputTokens;
}
void InputTokenCounter::recordServerUsage(int promptTokens)
{
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
return; return;
const double rawFactor m_serverInputTokens = serverInput;
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate); m_hasServerUsage = true;
const double clamped = std::clamp(rawFactor, 0.5, 3.0); recompute();
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped; }
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
.arg(promptTokens)
.arg(m_lastSentEstimate)
.arg(rawFactor, 0, 'f', 3)
.arg(m_calibrationFactor, 0, 'f', 3));
void InputTokenCounter::resetServerUsage()
{
m_serverInputTokens = 0;
m_hasServerUsage = false;
recompute(); recompute();
} }

View File

@@ -7,21 +7,25 @@
#include <QObject> #include <QObject>
#include <QStringList> #include <QStringList>
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Context { namespace QodeAssist::Context {
class ContextManager; class ContextManager;
} }
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class InputTokenCounter : public QObject class InputTokenCounter : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
InputTokenCounter( InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr); ConversationHistory *history,
Context::ContextManager *contextManager,
QObject *parent = nullptr);
int inputTokens() const; int inputTokens() const;
@@ -30,25 +34,22 @@ public:
void setLinkedFiles(const QStringList &linkedFiles); void setLinkedFiles(const QStringList &linkedFiles);
void recompute(); void recompute();
void recordSent(); void recordServerUsage(int promptTokens, int cachedTokens);
void recordServerUsage(int promptTokens); void resetServerUsage();
signals: signals:
void inputTokensChanged(); void inputTokensChanged();
private: private:
void rewireToolsChangedConnection(); ConversationHistory *m_history;
ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QMetaObject::Connection m_toolsChangedConn;
QStringList m_attachments; QStringList m_attachments;
QStringList m_linkedFiles; QStringList m_linkedFiles;
int m_messageTokens{0}; int m_messageTokens{0};
int m_inputTokens{0}; int m_inputTokens{0};
int m_lastSentEstimate{0}; int m_serverInputTokens{0};
double m_calibrationFactor{1.0}; bool m_hasServerUsage{false};
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -8,29 +8,43 @@
#include <QFileInfo> #include <QFileInfo>
#include <Session.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
SessionFileRegistry::SessionFileRegistry(QObject *parent) SessionFileRegistry::SessionFileRegistry(QObject *parent)
: QObject(parent) : QObject(parent)
{} {}
SessionFileRegistry::~SessionFileRegistry() = default;
bool SessionFileRegistry::isLocked(const QString &path) const bool SessionFileRegistry::isLocked(const QString &path) const
{ {
return !path.isEmpty() && m_lockedPaths.contains(path); return !path.isEmpty() && !m_locks.value(path).isNull();
} }
bool SessionFileRegistry::lock(const QString &path) bool SessionFileRegistry::isLockedByOther(const QString &path, QodeAssist::Session *self) const
{ {
if (path.isEmpty() || m_lockedPaths.contains(path)) { if (path.isEmpty())
return false; return false;
} const auto owner = m_locks.value(path);
m_lockedPaths.insert(path); return !owner.isNull() && owner != self;
}
bool SessionFileRegistry::lock(const QString &path, QodeAssist::Session *owner)
{
if (path.isEmpty())
return false;
const auto existing = m_locks.value(path);
if (!existing.isNull() && existing != owner)
return false;
m_locks.insert(path, owner);
return true; return true;
} }
void SessionFileRegistry::release(const QString &path) void SessionFileRegistry::release(const QString &path)
{ {
m_lockedPaths.remove(path); m_locks.remove(path);
} }
void SessionFileRegistry::setPendingChatFile(const QString &path) void SessionFileRegistry::setPendingChatFile(const QString &path)
@@ -45,7 +59,7 @@ QString SessionFileRegistry::takePendingChatFile()
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
{ {
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) { if (desiredPath.isEmpty() || !isLocked(desiredPath)) {
return desiredPath; return desiredPath;
} }
@@ -59,7 +73,7 @@ QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
if (!suffix.isEmpty()) { if (!suffix.isEmpty()) {
candidate += '.' + suffix; candidate += '.' + suffix;
} }
if (!m_lockedPaths.contains(candidate)) { if (!isLocked(candidate)) {
return candidate; return candidate;
} }
} }

View File

@@ -4,35 +4,37 @@
#pragma once #pragma once
#include <QHash>
#include <QObject> #include <QObject>
#include <QSet> #include <QPointer>
#include <QString> #include <QString>
namespace QodeAssist {
class Session;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
// Shared registry of chat session (autosave) file paths that are currently held by a live
// chat instance. Lets every chat view — bottom pane, navigation panel, editor split — claim
// a unique history file so two sessions never autosave into the same path.
class SessionFileRegistry : public QObject class SessionFileRegistry : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit SessionFileRegistry(QObject *parent = nullptr); explicit SessionFileRegistry(QObject *parent = nullptr);
~SessionFileRegistry() override;
bool isLocked(const QString &path) const; bool isLocked(const QString &path) const;
bool lock(const QString &path); bool isLockedByOther(const QString &path, QodeAssist::Session *self) const;
bool lock(const QString &path, QodeAssist::Session *owner);
void release(const QString &path); void release(const QString &path);
QString uniqueFreePath(const QString &desiredPath) const; QString uniqueFreePath(const QString &desiredPath) const;
// Handoff slot for relocating a live chat between hosts (split <-> window): the source
// chat stores its history file here, the freshly created host picks it up exactly once.
void setPendingChatFile(const QString &path); void setPendingChatFile(const QString &path);
QString takePendingChatFile(); QString takePendingChatFile();
private: private:
QSet<QString> m_lockedPaths; QHash<QString, QPointer<QodeAssist::Session>> m_locks;
QString m_pendingChatFile; QString m_pendingChatFile;
}; };

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 233 B

View File

@@ -1,9 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.75 2.75H8.25C6.73122 2.75 5.5 3.98122 5.5 5.5V38.5C5.5 40.0188 6.73122 41.25 8.25 41.25H35.75C37.2688 41.25 38.5 40.0188 38.5 38.5V5.5C38.5 3.98122 37.2688 2.75 35.75 2.75Z" stroke="black" stroke-width="4"/>
<path d="M13.75 14.4375C14.8891 14.4375 15.8125 13.5141 15.8125 12.375C15.8125 11.2359 14.8891 10.3125 13.75 10.3125C12.6109 10.3125 11.6875 11.2359 11.6875 12.375C11.6875 13.5141 12.6109 14.4375 13.75 14.4375Z" fill="black"/>
<path d="M19.25 12.375H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.75 24.0625C14.8891 24.0625 15.8125 23.1391 15.8125 22C15.8125 20.8609 14.8891 19.9375 13.75 19.9375C12.6109 19.9375 11.6875 20.8609 11.6875 22C11.6875 23.1391 12.6109 24.0625 13.75 24.0625Z" fill="black"/>
<path d="M19.25 22H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.75 33.6875C14.8891 33.6875 15.8125 32.7641 15.8125 31.625C15.8125 30.4859 14.8891 29.5625 13.75 29.5625C12.6109 29.5625 11.6875 30.4859 11.6875 31.625C11.6875 32.7641 12.6109 33.6875 13.75 33.6875Z" fill="black"/>
<path d="M19.25 31.625H27.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -118,7 +118,6 @@ ChatRootView {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved") text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
} }
openChatHistory.onClicked: root.openChatHistoryFolder() openChatHistory.onClicked: root.openChatHistoryFolder()
contextButton.onClicked: contextViewer.open()
pinButton { pinButton {
visible: typeof _chatview !== 'undefined' visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
@@ -138,43 +137,18 @@ ChatRootView {
relocateTooltip.text: (typeof _chatview !== 'undefined') relocateTooltip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor tab") ? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window") : qsTr("Move this chat to a separate window")
toolsButton {
checked: root.useTools
onCheckedChanged: {
root.useTools = toolsButton.checked
}
}
thinkingMode {
checked: root.useThinking
enabled: root.isThinkingSupport
onCheckedChanged: {
root.useThinking = thinkingMode.checked
}
}
settingsButton.onClicked: root.openSettings() settingsButton.onClicked: root.openSettings()
configSelector { agentSelector {
model: root.availableConfigurations model: root.availableChatAgents
displayText: root.currentConfiguration displayText: root.currentChatAgent
onActivated: function(index) { onActivated: function(index) {
if (index > 0) { root.currentChatAgent = root.availableChatAgents[index]
root.applyConfiguration(root.availableConfigurations[index])
}
} }
Component.onCompleted: root.loadAvailableChatAgents()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableConfigurations() root.loadAvailableChatAgents()
}
}
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
} }
} }
} }
@@ -343,7 +317,7 @@ ChatRootView {
onResetChatToMessage: function(idx) { onResetChatToMessage: function(idx) {
messageInput.text = model.content messageInput.text = model.content
messageInput.cursorPosition = model.content.length messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx) root.resetChatTo(idx)
} }
onOpenFileRequested: function(filePath) { onOpenFileRequested: function(filePath) {
@@ -593,6 +567,8 @@ ChatRootView {
isCompressing: root.isCompressing isCompressing: root.isCompressing
isProcessing: root.isRequestInProgress isProcessing: root.isRequestInProgress
canCompress: root.canCompress
canSend: root.currentChatAgent !== ""
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage() sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest() : root.cancelRequest()
sendButton.icon.source: root.isRequestInProgress sendButton.icon.source: root.isRequestInProgress
@@ -604,9 +580,11 @@ ChatRootView {
? root.errorColor : "transparent" ? root.errorColor : "transparent"
sendButtonTooltip.text: root.isRequestInProgress sendButtonTooltip.text: root.isRequestInProgress
? qsTr("Stop") ? qsTr("Stop")
: (root.currentChatAgent === ""
? qsTr("Assign a chat agent in the Pipelines settings")
: (root.hasActiveError : (root.hasActiveError
? root.lastErrorMessage ? root.lastErrorMessage
: qsTr("Send message to LLM %1").arg(root.sendShortcutText)) : qsTr("Send message to LLM %1").arg(root.sendShortcutText)))
compressButton.onClicked: compressConfirmDialog.open() compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression() cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles { syncOpenFiles {
@@ -681,6 +659,10 @@ ChatRootView {
} }
function sendChatMessage() { function sendChatMessage() {
if (root.isCompressing)
return
if (messageInput.text.trim() === "" && root.attachmentFiles.length === 0)
return
root.hasActiveError = false root.hasActiveError = false
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text)) root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = "" messageInput.text = ""
@@ -831,30 +813,6 @@ ChatRootView {
toastTextColor: "#FFFFFF" toastTextColor: "#FFFFFF"
} }
ContextViewer {
id: contextViewer
width: Math.min(parent.width * 0.85, 800)
height: Math.min(parent.height * 0.85, 700)
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 { Connections {
target: root target: root
function onLastErrorMessageChanged() { function onLastErrorMessageChanged() {

View File

@@ -21,6 +21,8 @@ Rectangle {
property bool isCompressing: false property bool isCompressing: false
property bool isProcessing: false property bool isProcessing: false
property bool canCompress: true
property bool canSend: true
property alias sendButtonTooltip: sendButtonTooltipId property alias sendButtonTooltip: sendButtonTooltipId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
@@ -139,10 +141,18 @@ Rectangle {
} }
} }
Item {
id: compressButtonContainer
visible: !root.isCompressing
implicitWidth: compressButtonId.implicitWidth
implicitHeight: compressButtonId.implicitHeight
QoAButton { QoAButton {
id: compressButtonId id: compressButtonId
visible: !root.isCompressing anchors.fill: parent
enabled: root.canCompress
text: qsTr("Compress") text: qsTr("Compress")
icon { icon {
@@ -150,17 +160,32 @@ Rectangle {
height: 15 height: 15
width: 15 width: 15
} }
}
HoverHandler {
id: compressHoverHandler
}
QoAToolTip { QoAToolTip {
visible: compressButtonId.hovered visible: compressHoverHandler.hovered
delay: 250 delay: 250
text: qsTr("Compress chat (create summarized copy using LLM)") text: root.canCompress
? qsTr("Compress chat (create summarized copy using LLM)")
: qsTr("Assign a compression agent in the Pipelines settings")
} }
} }
Item {
id: sendButtonContainer
implicitWidth: sendButtonId.implicitWidth
implicitHeight: sendButtonId.implicitHeight
QoAButton { QoAButton {
id: sendButtonId id: sendButtonId
anchors.fill: parent
enabled: root.isProcessing || (root.canSend && !root.isCompressing)
leftPadding: root.isProcessing ? 22 : 4 leftPadding: root.isProcessing ? 22 : 4
icon { icon {
@@ -178,11 +203,16 @@ Rectangle {
height: 14 height: 14
running: root.isProcessing running: root.isProcessing
} }
}
HoverHandler {
id: sendHoverHandler
}
QoAToolTip { QoAToolTip {
id: sendButtonTooltipId id: sendButtonTooltipId
visible: sendButtonId.hovered visible: sendHoverHandler.hovered
delay: 250 delay: 250
} }
} }

View File

@@ -1,543 +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
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property string baseSystemPrompt
property string currentAgentRole
property string currentAgentRoleDescription
property string currentAgentRoleSystemPrompt
property var activeRules
property int activeRulesCount
property string selectedRuleContent
signal openSettings()
signal openAgentRolesSettings()
signal openRulesFolder()
signal refreshRules()
signal ruleSelected(int index)
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Chat Context")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Flickable {
id: mainFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: sectionsColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: sectionsColumn
width: mainFlickable.width
spacing: 8
CollapsibleSection {
id: systemPromptSection
Layout.fillWidth: true
title: qsTr("Base System Prompt")
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 5
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
Flickable {
id: systemPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: systemPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: systemPromptText
width: systemPromptFlickable.width
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.baseSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
}
QoAButton {
text: qsTr("Edit in Settings")
onClicked: {
root.openSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: agentRoleSection
Layout.fillWidth: true
title: qsTr("Agent Role")
badge: root.currentAgentRole
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
Text {
text: root.currentAgentRoleDescription
font.pixelSize: 11
font.italic: true
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleDescription.length > 0
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
visible: root.currentAgentRoleSystemPrompt.length > 0
Flickable {
id: agentPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: agentPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: agentPromptText
width: agentPromptFlickable.width
text: root.currentAgentRoleSystemPrompt
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
Text {
text: qsTr("No role selected. Using base system prompt only.")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleSystemPrompt.length === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.currentAgentRoleSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
}
QoAButton {
text: qsTr("Manage Roles")
onClicked: {
root.openAgentRolesSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: projectRulesSection
Layout.fillWidth: true
title: qsTr("Project Rules")
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
SplitView {
Layout.fillWidth: true
Layout.preferredHeight: 220
orientation: Qt.Horizontal
visible: root.activeRulesCount > 0
Rectangle {
SplitView.minimumWidth: 120
SplitView.preferredWidth: 180
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules (%1)").arg(rulesList.count)
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
boundsBehavior: Flickable.StopAtBounds
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
height: ruleItemContent.implicitHeight + 8
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted)
return palette.highlight
if (parent.hovered)
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
id: ruleItemContent
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 10
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: modelData.category
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
root.ruleSelected(index)
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
Rectangle {
SplitView.fillWidth: true
SplitView.minimumWidth: 200
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: root.selectedRuleContent.length > 0
onClicked: utils.copyToClipboard(root.selectedRuleContent)
}
}
Flickable {
id: ruleContentFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: ruleContentArea.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: ruleContentArea
width: ruleContentFlickable.width
text: root.selectedRuleContent
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
}
Text {
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
visible: root.activeRulesCount === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Open Rules Folder")
onClicked: root.openRulesFolder()
}
}
}
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Text {
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
component CollapsibleSection: ColumnLayout {
id: sectionRoot
property string title
property string badge
property color badgeColor: palette.mid
property Component sectionContent: null
property bool expanded: false
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
border.color: palette.mid
border.width: 1
radius: 2
MouseArea {
id: sectionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: sectionRoot.expanded = !sectionRoot.expanded
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
text: sectionRoot.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.text
}
Text {
text: sectionRoot.title
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
Rectangle {
implicitWidth: badgeText.implicitWidth + 12
implicitHeight: 18
color: sectionRoot.badgeColor
radius: 3
Text {
id: badgeText
anchors.centerIn: parent
text: sectionRoot.badge
font.pixelSize: 10
color: "#FFFFFF"
}
}
}
}
Loader {
id: contentLoader
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 8
Layout.bottomMargin: 4
sourceComponent: sectionRoot.sectionContent
visible: sectionRoot.expanded
active: sectionRoot.expanded
}
}
onOpened: {
if (root.activeRulesCount > 0) {
root.ruleSelected(0)
}
}
}

View File

@@ -22,12 +22,8 @@ Rectangle {
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId property alias agentSelector: agentSelectorId
property alias roleSelector: roleSelector
property alias relocateTooltip: relocateTooltipId property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
@@ -134,7 +130,7 @@ Rectangle {
} }
QoAComboBox { QoAComboBox {
id: configSelectorId id: agentSelectorId
implicitHeight: 25 implicitHeight: 25
@@ -142,87 +138,17 @@ Rectangle {
currentIndex: 0 currentIndex: 0
QoAToolTip { QoAToolTip {
visible: configSelectorId.hovered visible: agentSelectorId.hovered
delay: 250 delay: 250
text: qsTr("Switch saved AI configuration") text: qsTr("Select chat agent (provider and model come from the agent)")
} }
} }
QoAComboBox {
id: roleSelector
implicitHeight: 25
model: []
currentIndex: 0
QoAToolTip {
visible: roleSelector.hovered
delay: 250
text: qsTr("Switch agent role (different system prompts)")
}
}
} }
Row { Row {
spacing: 10 spacing: 10
QoAButton {
id: toolsButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: toolsButtonId.hovered
delay: 250
text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return toolsButtonId.checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: thinkingModeId.hovered
delay: 250
text: thinkingModeId.enabled
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
}
QoAButton { QoAButton {
id: settingsButtonId id: settingsButtonId
@@ -332,23 +258,6 @@ Rectangle {
QoASeparator {} QoASeparator {}
QoAButton {
id: contextButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: contextButtonId.hovered
delay: 250
text: qsTr("View chat context (system prompt, role, rules)")
}
}
Badge { Badge {
id: tokensBadgeId id: tokensBadgeId

View File

@@ -209,24 +209,4 @@ QString CodeHandler::detectLanguageFromExtension(const QString &extension)
return extensionToLanguage.value(extension.toLower(), ""); return extensionToLanguage.value(extension.toLower(), "");
} }
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
{
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -32,10 +32,6 @@ public:
private: private:
static QString getCommentPrefix(const QString &language); static QString getCommentPrefix(const QString &language);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex();
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

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

View File

@@ -1,48 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include "pluginllmcore/PromptTemplateManager.hpp"
#include "pluginllmcore/ProvidersManager.hpp"
#include "settings/GeneralSettings.hpp"
namespace QodeAssist {
class ConfigurationManager : public QObject
{
Q_OBJECT
public:
static ConfigurationManager &instance();
void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots:
void selectProvider();
void selectModel();
void selectTemplate();
void selectUrl();
private:
explicit ConfigurationManager(QObject *parent = nullptr);
~ConfigurationManager() = default;
ConfigurationManager(const ConfigurationManager &) = delete;
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::ProvidersManager &m_providersManager;
PluginLLMCore::PromptTemplateManager &m_templateManger;
void setupConnections();
};
} // namespace QodeAssist

View File

@@ -5,9 +5,29 @@
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument> #include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply> #include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include "sources/common/ContextData.hpp"
#include <Agent.hpp>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <PluginBlocks.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <memory>
#include <vector>
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp" #include "context/DocumentContextReader.hpp"
@@ -15,30 +35,29 @@
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <pluginllmcore/RulesLoader.hpp> #include "sources/settings/PipelinesConfig.hpp"
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry, AgentFactory &agentFactory,
PluginLLMCore::IPromptProvider *promptProvider, SessionManager &sessionManager,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger) IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings) : m_generalSettings(generalSettings)
, m_completeSettings(completeSettings) , m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry) , m_agentFactory(agentFactory)
, m_promptProvider(promptProvider) , m_sessionManager(sessionManager)
, m_documentReader(documentReader) , m_documentReader(documentReader)
, m_performanceLogger(performanceLogger) , m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
{ {}
}
LLMClientInterface::~LLMClientInterface() LLMClientInterface::~LLMClientInterface()
{ {
handleCancelRequest(); cancelAllRequests();
} }
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
@@ -51,48 +70,34 @@ void LLMClientInterface::startImpl()
emit started(); emit started();
} }
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText) void LLMClientInterface::onCompletionFinished(const QString &requestId)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value(); QString fullText;
sendCompletionToClient(fullText, ctx.originalRequest, true); if (Session *session = it.value().session) {
if (auto *history = session->history(); history && !history->isEmpty())
fullText = history->messages().back().text();
}
const QJsonObject originalRequest = it.value().originalRequest;
m_activeRequests.erase(it); sendCompletionToClient(fullText, originalRequest, true);
m_performanceLogger.endTimeMeasurement(requestId); finishRequest(requestId);
} }
void LLMClientInterface::handleRequestFinalized( void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error)
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"]; response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
QJsonObject errorObject; QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code errorObject["code"] = -32603; // Internal error code
@@ -100,9 +105,21 @@ void LLMClientInterface::handleRequestFailed(const QString &requestId, const QSt
response["error"] = errorObject; response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
finishRequest(requestId);
}
void LLMClientInterface::finishRequest(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
m_activeRequests.erase(it); m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId); m_performanceLogger.endTimeMeasurement(requestId);
if (session)
m_sessionManager.release(session);
} }
void LLMClientInterface::sendData(const QByteArray &data) void LLMClientInterface::sendData(const QByteArray &data)
@@ -125,7 +142,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
} else if (method == "getCompletionsCycling") { } else if (method == "getCompletionsCycling") {
handleCompletion(request); handleCompletion(request);
} else if (method == "$/cancelRequest") { } else if (method == "$/cancelRequest") {
handleCancelRequest(); handleCancelRequest(request);
} else if (method == "exit") { } else if (method == "exit") {
// TODO make exit handler // TODO make exit handler
} else { } else {
@@ -133,28 +150,50 @@ void LLMClientInterface::sendData(const QByteArray &data)
} }
} }
void LLMClientInterface::handleCancelRequest() void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
{ {
QSet<PluginLLMCore::Provider *> providers; const QJsonValue lspId = request["params"].toObject()["id"];
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) { QString matchedKey;
providers.insert(it.value().provider); for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
if (it.value().originalRequest["id"] == lspId) {
matchedKey = it.key();
break;
} }
} }
if (matchedKey.isEmpty()) {
LOG_MESSAGE(QString("No active completion request to cancel for LSP id %1")
.arg(lspId.toVariant().toString()));
return;
}
for (auto *provider : providers) { finishRequest(matchedKey);
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { QJsonObject response;
const RequestContext &ctx = it.value(); response["jsonrpc"] = "2.0";
if (ctx.provider) { response[LanguageServerProtocol::idKey] = lspId;
ctx.provider->cancelRequest(it.key());
}
}
QJsonObject errorObject;
errorObject["code"] = -32800;
errorObject["message"] = QStringLiteral("Request cancelled");
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
LOG_MESSAGE(QString("Cancelled completion request %1").arg(matchedKey));
}
void LLMClientInterface::cancelAllRequests()
{
const auto requests = m_activeRequests;
m_activeRequests.clear(); m_activeRequests.clear();
for (auto it = requests.begin(); it != requests.end(); ++it) {
m_performanceLogger.endTimeMeasurement(it.key());
if (Session *session = it.value().session)
m_sessionManager.release(session);
}
LOG_MESSAGE("All requests cancelled and state cleared"); LOG_MESSAGE("All requests cancelled and state cleared");
} }
@@ -193,21 +232,6 @@ void LLMClientInterface::handleShutdown(const QJsonObject &request)
void LLMClientInterface::handleTextDocumentDidOpen(const QJsonObject &request) {} void LLMClientInterface::handleTextDocumentDidOpen(const QJsonObject &request) {}
void LLMClientInterface::handleInitialized(const QJsonObject &request)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response["method"] = "initialized";
response["params"] = QJsonObject();
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::handleExit(const QJsonObject &request)
{
emit finished();
}
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage) void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
{ {
QJsonObject response; QJsonObject response;
@@ -220,10 +244,6 @@ void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QSt
response["error"] = errorObject; response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
// End performance measurement if it was started
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
} }
void LLMClientInterface::handleCompletion(const QJsonObject &request) void LLMClientInterface::handleCompletion(const QJsonObject &request)
@@ -237,133 +257,103 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return; return;
} }
auto updatedContext = prepareContext(request, documentInfo); const QString agentName = pickCompletionAgent(filePath);
if (agentName.isEmpty()) {
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo); QString error = QString("No code completion agent matches: %1").arg(filePath);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() QString sessionError;
: m_generalSettings.ccPreset1Template(); Session *session = m_sessionManager.acquire(agentName, &sessionError);
if (!session) {
LOG_MESSAGE(sessionError);
sendErrorResponse(request, sessionError);
return;
}
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); Templates::ContextRenderer::Bindings bindings;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
bindings.projectDir = project->projectDirectory().toFSPathString();
bindings.configDir = AgentFactory::userConfigDir();
bindings.language = CodeHandler::detectLanguageFromExtension(QFileInfo(filePath).suffix());
session->setContextBindings(bindings);
if (!promptTemplate) { Templates::ContextData context = prepareContext(request, documentInfo);
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())
session->systemPrompt()->setLayer(QStringLiteral("completion.context"), editorContext);
connect(
session,
&Session::finished,
this,
[this, session](const LLMQore::RequestID &, const QString &) {
onCompletionFinished(requestIdForSession(session));
});
connect(
session,
&Session::failed,
this,
[this, session](const LLMQore::RequestID &, const QodeAssist::ErrorInfo &error) {
onCompletionFailed(requestIdForSession(session), error.message);
});
if (auto *client = session->client())
client->setTransferTimeout(static_cast<int>(m_generalSettings.requestTimeout() * 1000));
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(
std::make_unique<CompletionContent>(
context.prefix.value_or(QString()), context.suffix.value_or(QString())));
const LLMQore::RequestID requestId = session->send(std::move(blocks));
if (requestId.isEmpty()) {
QString error = QString("Failed to start completion request for agent '%1': %2")
.arg(agentName, session->lastError().message);
m_sessionManager.removeSession(session);
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
QJsonObject payload{{"model", modelName}, {"stream", true}}; m_activeRequests[requestId] = {request, session};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for completion");
}
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<PluginLLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
PluginLLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&LLMClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_performanceLogger.startTimeMeasurement(requestId); m_performanceLogger.startTimeMeasurement(requestId);
} }
PluginLLMCore::ContextData LLMClientInterface::prepareContext( QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
{
const QStringList roster = Settings::PipelinesConfig::loadCached().rosters.codeCompletion;
if (roster.isEmpty())
return {};
AgentRouter::Context ctx;
ctx.filePath = filePath;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
ctx.projectName = project->displayName();
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
}
QString LLMClientInterface::requestIdForSession(Session *session) const
{
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
if (it.value().session == session)
return it.key();
}
return {};
}
Templates::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo) const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
@@ -377,14 +367,6 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings); return 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();
}
Context::ContextManager *LLMClientInterface::contextManager() const Context::ContextManager *LLMClientInterface::contextManager() const
{ {
return m_contextManager; return m_contextManager;
@@ -393,15 +375,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
void LLMClientInterface::sendCompletionToClient( void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete) const QString &completion, const QJsonObject &request, bool isComplete)
{ {
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response; QJsonObject response;
@@ -420,13 +393,12 @@ void LLMClientInterface::sendCompletionToClient(
if (outputHandler == "Raw text") { if (outputHandler == "Raw text") {
processedCompletion = completion; processedCompletion = completion;
} else if (outputHandler == "Force processing") { } else if (outputHandler == "Force processing") {
processedCompletion = CodeHandler::processText(completion, processedCompletion
Context::extractFilePathFromRequest(request)); = CodeHandler::processText(completion, Context::extractFilePathFromRequest(request));
} else { // "Auto" } else { // "Auto"
processedCompletion = CodeHandler::hasCodeBlocks(completion) processedCompletion
? CodeHandler::processText(completion, = CodeHandler::hasCodeBlocks(completion)
Context::extractFilePathFromRequest( ? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
request))
: completion; : completion;
} }
@@ -459,8 +431,6 @@ void LLMClientInterface::sendCompletionToClient(
QString("Full response: \n%1") QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
} }

View File

@@ -8,21 +8,25 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <QPointer>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp> #include <context/ProgrammingLanguage.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp> #include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
class QNetworkReply;
class QNetworkAccessManager;
namespace QodeAssist { namespace QodeAssist {
class AgentFactory;
class Session;
class SessionManager;
namespace Templates {
struct ContextData;
}
class LLMClientInterface : public LanguageClient::BaseClientInterface class LLMClientInterface : public LanguageClient::BaseClientInterface
{ {
Q_OBJECT Q_OBJECT
@@ -31,8 +35,8 @@ public:
LLMClientInterface( LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry, AgentFactory &agentFactory,
PluginLLMCore::IPromptProvider *promptProvider, SessionManager &sessionManager,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger); IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override; ~LLMClientInterface() override;
@@ -44,7 +48,6 @@ public:
void handleCompletion(const QJsonObject &request); void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override; void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const; Context::ContextManager *contextManager() const;
@@ -52,40 +55,36 @@ public:
protected: protected:
void startImpl() override; void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private: private:
void handleInitialize(const QJsonObject &request); void handleInitialize(const QJsonObject &request);
void handleShutdown(const QJsonObject &request); void handleShutdown(const QJsonObject &request);
void handleTextDocumentDidOpen(const QJsonObject &request); void handleTextDocumentDidOpen(const QJsonObject &request);
void handleInitialized(const QJsonObject &request); void handleCancelRequest(const QJsonObject &request);
void handleExit(const QJsonObject &request); void cancelAllRequests();
void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage); void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
void onCompletionFinished(const QString &requestId);
void onCompletionFailed(const QString &requestId, const QString &error);
void finishRequest(const QString &requestId);
QString requestIdForSession(Session *session) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
}; };
PluginLLMCore::ContextData prepareContext( Templates::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint( QString pickCompletionAgent(const QString &filePath) const;
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings; const Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; const Settings::CodeCompletionSettings &m_completeSettings;
PluginLLMCore::IProviderRegistry &m_providerRegistry; AgentFactory &m_agentFactory;
SessionManager &m_sessionManager;
Context::IDocumentReader &m_documentReader; Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger; IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
}; };

View File

@@ -35,7 +35,6 @@ namespace QodeAssist {
class Completion : public LanguageServerProtocol::JsonObject class Completion : public LanguageServerProtocol::JsonObject
{ {
static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"}; static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"};
static constexpr LanguageServerProtocol::Key uuidKey{"uuid"};
public: public:
using JsonObject::JsonObject; using JsonObject::JsonObject;
@@ -55,7 +54,6 @@ public:
} }
QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); } QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); }
void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); } void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); }
QString uuid() const { return typedValue<QString>(uuidKey); }
bool isValid() const override bool isValid() const override
{ {

View File

@@ -159,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this); m_refactorWidgetHandler = new RefactorWidgetHandler(this);
} }
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
QodeAssistClient::~QodeAssistClient() QodeAssistClient::~QodeAssistClient()
{ {
cleanupConnections(); cleanupConnections();
@@ -262,10 +272,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager()->shouldIgnore(
if (m_llmClient->contextManager() editor->textDocument()->filePath().toUrlishString())) {
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -297,6 +305,12 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
QTC_ASSERT(editor, return); QTC_ASSERT(editor, return);
handleCompletions(response, editor); handleCompletions(response, editor);
}); });
connect(
editor,
&TextEditorWidget::destroyed,
this,
&QodeAssistClient::onEditorDestroyed,
Qt::UniqueConnection);
m_runningRequests[editor] = request; m_runningRequests[editor] = request;
sendMessage(request); sendMessage(request);
} }
@@ -309,9 +323,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager() if (m_llmClient->contextManager()->shouldIgnore(
->ignoreManager() editor->textDocument()->filePath().toUrlishString())) {
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -319,6 +332,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!m_refactorHandler) { if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this); m_refactorHandler = new QuickRefactorHandler(this);
m_refactorHandler->setSessionManager(m_sessionManager);
m_refactorHandler->setAgentFactory(m_agentFactory);
connect( connect(
m_refactorHandler, m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted, &QuickRefactorHandler::refactoringCompleted,
@@ -357,10 +372,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
return; return;
requestCompletions(editor); requestCompletions(editor);
}); });
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() { connect(
delete m_scheduledRequests.take(editor); editor,
cancelRunningRequest(editor); &TextEditorWidget::destroyed,
}); this,
&QodeAssistClient::onEditorDestroyed,
Qt::UniqueConnection);
it = m_scheduledRequests.insert(editor, timer); it = m_scheduledRequests.insert(editor, timer);
} }
@@ -376,9 +393,12 @@ void QodeAssistClient::handleCompletions(
editor->abortAssist(); editor->abortAssist();
if (response.error()) { if (response.error()) {
m_runningRequests.remove(editor);
log(*response.error()); log(*response.error());
if (response.error()->code() != -32800) {
m_errorHandler m_errorHandler
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message())); .showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
}
return; return;
} }
@@ -479,6 +499,13 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
m_runningRequests.erase(it); m_runningRequests.erase(it);
} }
void QodeAssistClient::onEditorDestroyed(QObject *editorObject)
{
auto *editor = static_cast<TextEditor::TextEditorWidget *>(editorObject);
delete m_scheduledRequests.take(editor);
cancelRunningRequest(editor);
}
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
{ {
if (!project) if (!project)
@@ -520,6 +547,11 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{ {
m_progressHandler.hideProgress(); m_progressHandler.hideProgress();
if (result.cancelled) {
LOG_MESSAGE("Refactoring request was cancelled");
return;
}
if (!result.success) { if (!result.success) {
QString errorMessage = result.errorMessage.isEmpty() QString errorMessage = result.errorMessage.isEmpty()
? tr("Quick refactor failed") ? tr("Quick refactor failed")
@@ -538,6 +570,15 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
return; return;
} }
if (result.documentRevision >= 0
&& result.editor->document()->revision() != result.documentRevision) {
m_errorHandler.showError(
result.editor,
tr("Quick refactor discarded: the document changed while the request was running"));
LOG_MESSAGE("Refactoring result discarded: document revision changed");
return;
}
int displayMode = Settings::quickRefactorSettings().displayMode(); int displayMode = Settings::quickRefactorSettings().displayMode();
if (displayMode == 0) { if (displayMode == 0) {
@@ -634,7 +675,14 @@ void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
displayRefactored = result.newText; displayRefactored = result.newText;
} }
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) { const int revisionAtDisplay = editorWidget->document()->revision();
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result, revisionAtDisplay](
const QString &editedText) {
if (editorWidget->document()->revision() != revisionAtDisplay) {
m_errorHandler.showError(
editorWidget, tr("Quick refactor discarded: the document changed before applying"));
return;
}
applyRefactoringEdit(editorWidget, result.insertRange, editedText); applyRefactoringEdit(editorWidget, result.insertRange, editedText);
}); });

View File

@@ -6,21 +6,22 @@
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QPointer>
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp" #include "LSPCompletion.hpp"
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
#include "RefactorSuggestionHoverHandler.hpp" #include "RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp" #include "widgets/CompletionErrorHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp" #include "widgets/CompletionProgressHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp" #include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class AgentFactory;
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
Q_OBJECT Q_OBJECT
@@ -28,6 +29,9 @@ public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override; ~QodeAssistClient() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override; bool canOpenProject(ProjectExplorer::Project *project) override;
@@ -43,6 +47,7 @@ private:
void handleCompletions( void handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor); const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor); void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
void onEditorDestroyed(QObject *editorObject);
bool isEnabled(ProjectExplorer::Project *project) const; bool isEnabled(ProjectExplorer::Project *project) const;
void setupConnections(); void setupConnections();
@@ -63,11 +68,12 @@ private:
int m_recentCharCount; int m_recentCharCount;
CompletionProgressHandler m_progressHandler; CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler; CompletionErrorHandler m_errorHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr}; QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr}; RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient; LLMClientInterface *m_llmClient;
SessionManager *m_sessionManager{nullptr};
AgentFactory *m_agentFactory{nullptr};
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -4,23 +4,39 @@
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <QJsonArray> #include <LLMQore/ContentBlocks.hpp>
#include <QJsonDocument> #include <LLMQore/ToolLoopRunner.hpp>
#include <QUuid> #include <LLMQore/ToolsManager.hpp>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <context/DocumentContextReader.hpp> #include <context/DocumentContextReader.hpp>
#include <pluginllmcore/ResponseCleaner.hpp>
#include <context/DocumentReaderQtCreator.hpp> #include <context/DocumentReaderQtCreator.hpp>
#include <context/EnvBlockFormatter.hpp>
#include <context/Utils.hpp> #include <context/Utils.hpp>
#include <pluginllmcore/PromptTemplateManager.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <pluginllmcore/RulesLoader.hpp>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp> #include <settings/QuickRefactorSettings.hpp>
#include <settings/ToolsSettings.hpp> #include <settings/ToolsSettings.hpp>
#include <sources/common/ResponseCleaner.hpp>
#include "sources/common/ContextData.hpp"
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <ContextRenderer.hpp>
#include <ConversationHistory.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "sources/settings/PipelinesConfig.hpp"
#include "tools/ToolsRegistration.hpp"
namespace QodeAssist { namespace QodeAssist {
@@ -34,14 +50,25 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
QuickRefactorHandler::~QuickRefactorHandler() {} QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
void QuickRefactorHandler::sendRefactorRequest( void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions) TextEditor::TextEditorWidget *editor, const QString &instructions)
{ {
if (m_isRefactoringInProgress) { if (m_isRefactoringInProgress) {
cancelRequest(); abortActiveRequest();
} }
m_currentEditor = editor; m_currentEditor = editor;
m_currentDocumentRevision = editor->document()->revision();
Utils::Text::Range range; Utils::Text::Range range;
if (editor->textCursor().hasSelection()) { if (editor->textCursor().hasSelection()) {
@@ -88,105 +115,117 @@ void QuickRefactorHandler::sendRefactorRequest(
prepareAndSendRequest(editor, instructions, range); prepareAndSendRequest(editor, instructions, range);
} }
QString QuickRefactorHandler::configuredAgent(AgentFactory *agentFactory)
{
const QString configured = Settings::PipelinesConfig::loadCached().rosters.quickRefactor;
if (configured.isEmpty() || !agentFactory || !agentFactory->configByName(configured))
return {};
return configured;
}
QString QuickRefactorHandler::pickRefactorAgent() const
{
return configuredAgent(m_agentFactory);
}
void QuickRefactorHandler::prepareAndSendRequest( void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range) const Utils::Text::Range &range)
{ {
auto &settings = Settings::generalSettings(); const auto emitError = [this, editor](const QString &error) {
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
const auto providerName = settings.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
RefactorResult result; RefactorResult result;
result.success = false; result.success = false;
result.errorMessage = error; result.errorMessage = error;
result.editor = editor; result.editor = editor;
emit refactoringCompleted(result); emit refactoringCompleted(result);
};
if (!m_sessionManager) {
emitError(QStringLiteral("Quick refactor session manager is not available"));
return; return;
} }
const auto templateName = settings.qrTemplate(); const QString agentName = pickRefactorAgent();
auto promptTemplate = promptManager.getChatTemplateByName(templateName); if (agentName.isEmpty()) {
emitError(
if (!promptTemplate) { QStringLiteral("No quick refactor agent configured. Set one in QodeAssist > General."));
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return; return;
} }
QJsonObject payload{ QString sessionError;
{"model", Settings::generalSettings().qrModel()}, {"stream", true}}; Session *session = m_sessionManager->acquire(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(); auto *project = ProjectExplorer::ProjectManager::startupProject();
bool enableThinking = Settings::quickRefactorSettings().useThinking(); Templates::ContextRenderer::Bindings bindings;
provider->prepareRequest( bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
payload, bindings.configDir = AgentFactory::userConfigDir();
promptTemplate, session->setContextBindings(bindings);
context,
PluginLLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
provider->client()->setMaxToolContinuations( const AgentConfig *agentConfig = m_agentFactory ? m_agentFactory->configByName(agentName)
Settings::toolsSettings().maxToolContinuations()); : nullptr;
if (agentConfig && agentConfig->enableTools) {
m_sessionManager->toolContributors().contribute(client->tools());
client->toolLoop()->setMaxRounds(Settings::toolsSettings().maxToolContinuations());
}
provider->client()->setTransferTimeout( session->systemPrompt()->setLayer(QStringLiteral("refactor"), buildContextLayer(editor, range));
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_isRefactoringInProgress = true; m_isRefactoringInProgress = true;
connect(session, &Session::finished, this, [this](const LLMQore::RequestID &id, const QString &) {
onRefactorFinished(id);
});
connect( connect(
provider->client(), session,
&::LLMQore::BaseClient::requestCompleted, &Session::failed,
this, this,
&QuickRefactorHandler::handleFullResponse, [this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
Qt::UniqueConnection); onRefactorFailed(id, error);
});
connect( std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
provider->client(), const QString userMessage
&::LLMQore::BaseClient::requestFinalized, = instructions.isEmpty()
this, ? QStringLiteral("Refactor the code to improve its quality and maintainability.")
&QuickRefactorHandler::handleRequestFinalized, : instructions;
Qt::UniqueConnection); blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
connect( const LLMQore::RequestID requestId = session->send(std::move(blocks));
provider->client(), if (requestId.isEmpty()) {
&::LLMQore::BaseClient::requestFailed, m_isRefactoringInProgress = false;
this, const QString reason = session->lastError().message;
&QuickRefactorHandler::handleRequestFailed, m_sessionManager->removeSession(session);
Qt::UniqueConnection); emitError(QStringLiteral("Failed to start quick refactor request for agent '%1': %2")
.arg(agentName, reason));
return;
}
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
m_lastRequestId = requestId; m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}}; m_activeSession = session;
m_activeRequests[requestId] = {request, provider};
} }
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( QString QuickRefactorHandler::buildContextLayer(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
const Utils::Text::Range &range,
const QString &instructions)
{ {
PluginLLMCore::ContextData context; Q_UNUSED(range)
auto textDocument = editor->textDocument(); auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader; Context::DocumentReaderQtCreator documentReader;
@@ -194,7 +233,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
if (!documentInfo.document) { if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available"); LOG_MESSAGE("Error: Document is not available");
return context; return {};
} }
QTextCursor cursor = editor->textCursor(); QTextCursor cursor = editor->textCursor();
@@ -268,43 +307,21 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
taggedContent = contextBefore + "<cursor>" + contextAfter; taggedContent = contextBefore + "<cursor>" + contextAfter;
} }
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt(); QString contextLayer = Context::EnvBlockFormatter::formatFile(
{documentInfo.filePath, documentInfo.mimeType});
auto project = PluginLLMCore::RulesLoader::getActiveProject(); contextLayer += "\n# Code Context with Position Markers\n" + taggedContent;
if (project) {
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
project, PluginLLMCore::RulesContext::QuickRefactor);
if (!projectRules.isEmpty()) { contextLayer += "\n\n# What to Generate:";
systemPrompt += "\n\n# Project Rules\n\n" + projectRules; contextLayer
LOG_MESSAGE("Loaded project rules for quick refactor"); += cursor.hasSelection()
}
}
systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath;
systemPrompt += "\n\n# Code Context with Position Markers\n" + taggedContent;
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
systemPrompt += cursor.hasSelection()
? "\n- Generate ONLY the code that should REPLACE the selected text between " ? "\n- Generate ONLY the code that should REPLACE the selected text between "
"<selection_start> and <selection_end> markers" "<selection_start> and <selection_end> markers"
"\n- Your output will completely replace the selected code" "\n- Your output will completely replace the selected code"
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position" : "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
"\n- Your output will be inserted at the cursor location"; "\n- Your output will be inserted at the cursor location";
systemPrompt += "\n\n## Formatting Rules:" QString indentNote;
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
"\n- Do NOT add comments explaining what you changed"
"\n- Do NOT repeat existing code, be precise with context"
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
"\n- The output must be ready to insert directly into the editor as-is";
systemPrompt += "\n\n## Indentation and Whitespace:";
if (cursor.hasSelection()) { if (cursor.hasSelection()) {
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart()); QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
int leadingSpaces = 0; int leadingSpaces = 0;
@@ -314,12 +331,14 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
else break; else break;
} }
if (leadingSpaces > 0) { if (leadingSpaces > 0) {
systemPrompt += QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation" indentNote
= QString(
"\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)" "\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
"\n- Each line in your output must maintain this base indentation") "\n- Each line in your output must maintain this base indentation")
.arg(leadingSpaces); .arg(leadingSpaces);
} }
systemPrompt += "\n- PRESERVE all indentation from the original code"; indentNote += "\n- PRESERVE all indentation from the original code";
} else { } else {
QTextBlock block = documentInfo.document->findBlock(cursorPos); QTextBlock block = documentInfo.document->findBlock(cursorPos);
QString lineText = block.text(); QString lineText = block.text();
@@ -330,61 +349,37 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
else break; else break;
} }
if (leadingSpaces > 0) { if (leadingSpaces > 0) {
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation" indentNote = QString(
"\n- If generating multiline code, EVERY line must start with at least %1 spaces" "\n- CRITICAL: Current line has %1 spaces of indentation"
"\n- If generating single-line code, it will be inserted inline (no indentation needed)") "\n- If generating multiline code, EVERY line must start with at "
"least %1 spaces"
"\n- If generating single-line code, it will be inserted inline (no "
"indentation needed)")
.arg(leadingSpaces); .arg(leadingSpaces);
} }
} }
if (!indentNote.isEmpty())
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code" contextLayer += "\n\n## Indentation:" + indentNote;
"\n- Maintain consistent indentation for nested blocks"
"\n- Do NOT remove or reduce the base indentation level"
"\n\n## Code Style:"
"\n- Match the coding style of the surrounding code (naming, spacing, braces, etc.)"
"\n- Preserve the original code structure when possible"
"\n- Only change what is necessary to fulfill the user's request";
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) { if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath}); contextLayer += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
} }
context.systemPrompt = systemPrompt; return contextLayer;
QVector<PluginLLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
} }
void QuickRefactorHandler::handleLLMResponse( void QuickRefactorHandler::abortActiveRequest()
const QString &response, const QJsonObject &request, bool isComplete)
{ {
if (request["id"].toString() != m_lastRequestId) { if (!m_isRefactoringInProgress)
return; return;
}
if (isComplete) {
m_isRefactoringInProgress = false; m_isRefactoringInProgress = false;
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response); m_lastRequestId.clear();
RefactorResult result; Session *session = m_activeSession;
result.newText = cleanedResponse; m_activeSession = nullptr;
result.insertRange = m_currentRange; if (session && m_sessionManager)
result.success = true; m_sessionManager->release(session);
result.editor = m_currentEditor;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
} }
void QuickRefactorHandler::cancelRequest() void QuickRefactorHandler::cancelRequest()
@@ -392,60 +387,73 @@ void QuickRefactorHandler::cancelRequest()
if (!m_isRefactoringInProgress) if (!m_isRefactoringInProgress)
return; return;
const auto id = m_lastRequestId; abortActiveRequest();
RefactorResult result;
result.success = false;
result.cancelled = true;
result.errorMessage = "Refactoring request was cancelled";
result.editor = m_currentEditor;
emit refactoringCompleted(result);
}
void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
{
if (requestId != m_lastRequestId)
return;
Session *session = m_activeSession;
m_activeSession = nullptr;
QString fullText;
if (session) {
if (auto *history = session->history(); history && !history->isEmpty())
fullText = history->messages().back().text();
}
m_isRefactoringInProgress = false; m_isRefactoringInProgress = false;
m_lastRequestId.clear(); m_lastRequestId.clear();
auto it = m_activeRequests.find(id); const QString cleanedResponse = ResponseCleaner::clean(fullText);
if (it != m_activeRequests.end()) {
auto provider = it.value().provider;
m_activeRequests.erase(it);
if (provider)
provider->cancelRequest(id);
}
RefactorResult result; RefactorResult result;
result.success = false; result.newText = cleanedResponse;
result.errorMessage = "Refactoring request was cancelled"; result.insertRange = m_currentRange;
result.success = true;
result.editor = m_currentEditor;
result.documentRevision = m_currentDocumentRevision;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result); emit refactoringCompleted(result);
if (session && m_sessionManager)
m_sessionManager->release(session);
} }
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText) void QuickRefactorHandler::onRefactorFailed(
const QString &requestId, const QodeAssist::ErrorInfo &error)
{ {
if (requestId == m_lastRequestId) { if (requestId != m_lastRequestId)
m_activeRequests.remove(requestId);
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true);
}
}
void QuickRefactorHandler::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (requestId != m_lastRequestId || !info.usage)
return; return;
const auto &u = *info.usage; Session *session = m_activeSession;
LOG_MESSAGE( m_activeSession = nullptr;
QString("Quick refactor 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 QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
{
if (requestId == m_lastRequestId) {
m_activeRequests.remove(requestId);
m_isRefactoringInProgress = false; m_isRefactoringInProgress = false;
m_lastRequestId.clear();
RefactorResult result; RefactorResult result;
result.success = false; result.success = false;
result.errorMessage = error; result.errorMessage = error.message;
result.editor = m_currentEditor; result.editor = m_currentEditor;
emit refactoringCompleted(result); emit refactoringCompleted(result);
}
if (session && m_sessionManager)
m_sessionManager->release(session);
} }
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -4,27 +4,32 @@
#pragma once #pragma once
#include <QJsonObject>
#include <QObject> #include <QObject>
#include <QPointer>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <utils/textutils.h> #include <utils/textutils.h>
#include <ErrorInfo.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class Session;
class AgentFactory;
struct RefactorResult struct RefactorResult
{ {
QString newText; QString newText;
Utils::Text::Range insertRange; Utils::Text::Range insertRange;
bool success; bool success = false;
bool cancelled = false;
QString errorMessage; QString errorMessage;
TextEditor::TextEditorWidget *editor{nullptr}; QPointer<TextEditor::TextEditorWidget> editor;
int documentRevision = -1;
}; };
class QuickRefactorHandler : public QObject class QuickRefactorHandler : public QObject
@@ -35,43 +40,39 @@ public:
explicit QuickRefactorHandler(QObject *parent = nullptr); explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override; ~QuickRefactorHandler() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions); void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest(); void cancelRequest();
bool isProcessing() const { return m_isRefactoringInProgress; } bool isProcessing() const { return m_isRefactoringInProgress; }
static QString configuredAgent(AgentFactory *agentFactory);
signals: signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result); void refactoringCompleted(const QodeAssist::RefactorResult &result);
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private: private:
void prepareAndSendRequest( void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range); const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); void onRefactorFinished(const QString &requestId);
PluginLLMCore::ContextData prepareContext( void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
TextEditor::TextEditorWidget *editor, QString buildContextLayer(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
const Utils::Text::Range &range, QString pickRefactorAgent() const;
const QString &instructions); void abortActiveRequest();
struct RequestContext QPointer<SessionManager> m_sessionManager;
{ QPointer<AgentFactory> m_agentFactory;
QJsonObject originalRequest; QPointer<Session> m_activeSession;
PluginLLMCore::Provider *provider; QPointer<TextEditor::TextEditorWidget> m_currentEditor;
};
QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange; Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress; bool m_isRefactoringInProgress;
QString m_lastRequestId; QString m_lastRequestId;
int m_currentDocumentRevision = -1;
Context::ContextManager m_contextManager; Context::ContextManager m_contextManager;
}; };

149
README.md
View File

@@ -39,9 +39,9 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge) - **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet) - **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
- **File Context** — attach, link, or auto-sync open editor files for richer prompts - **File Context** — attach, link, or auto-sync open editor files for richer prompts
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint - **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen, DeepSeek, any OpenAI-compatible endpoint
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …) - **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control - **Customizable** — per-agent personas (agent TOML `system_prompt`), reusable refactor templates, full prompt-template control
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users! **Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
@@ -157,29 +157,17 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
## Configuration ## Configuration
### Quick Setup (Recommended for Beginners) ### Quick Setup
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps: QodeAssist is configured through three settings pages (Preferences > QodeAssist):
<details>
<summary>Quick setup: (click to expand)</summary>
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
</details>
1. **Open QodeAssist Settings** 1. **Providers** — pick a bundled provider instance (Claude, OpenAI, Google AI, Mistral, Ollama, …) and enter its API key. Local providers (Ollama, llama.cpp, LM Studio) need no key.
2. **Select a Preset** - Choose from the Quick Setup dropdown: 2. **Agents** — every feature runs an *agent*: a bundled TOML preset combining a provider, model, and request template. The bundled agents work out of the box; create your own by extending a base agent under a new name.
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5) 3. **General > Agent Pipelines** — assign agents to the four feature slots: code completion (ordered, routed per file), chat assistant (picker allow-list), chat compression, and quick refactor. Local Ollama agents are pre-assigned by default; an unassigned slot disables that feature.
- **OpenAI** (gpt-5.2-codex)
- **Mistral AI** (Codestral 2501)
- **Google AI** (Gemini 2.5 Flash)
- **Qwen** (Qwen3.6 Plus, Qwen3.7 Max)
- **DeepSeek** (DeepSeek V4 Flash, DeepSeek V4 Pro)
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go! ### Providers
### Manual Provider Configuration Choose your preferred provider:
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
**Local providers:** **Local providers:**
- **[Ollama](docs/ollama-configuration.md)** — native Ollama API - **[Ollama](docs/ollama-configuration.md)** — native Ollama API
@@ -193,8 +181,8 @@ For advanced users or local models, choose your preferred provider and follow th
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API - **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral** - **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
- **[Google AI](docs/google-ai-configuration.md)** — Gemini - **[Google AI](docs/google-ai-configuration.md)** — Gemini
- **Qwen (Alibaba)** — DashScope OpenAI-compatible Chat and Responses endpoints - **Qwen (Alibaba)** — DashScope OpenAI-compatible endpoint (`qwen-plus`, `qwen-max`, `qwen-coder`)
- **DeepSeek** — `deepseek-chat` and `deepseek-reasoner` (reasoning shown as thinking) - **DeepSeek** — OpenAI-compatible endpoint, `deepseek-chat` and `deepseek-reasoner`
- **OpenAI-compatible** — OpenRouter and any custom endpoint - **OpenAI-compatible** — OpenRouter and any custom endpoint
### Recommended Models for Best Experience ### Recommended Models for Best Experience
@@ -217,9 +205,8 @@ For optimal coding assistance, we recommend using these top-tier models:
### Additional Configuration ### Additional Configuration
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts - **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents (including personas via `system_prompt`) with TOML profiles
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens - **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore` - **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
## Features ## Features
@@ -255,7 +242,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- Multiple chat panels: side panel, bottom panel, and popup window - Multiple chat panels: side panel, bottom panel, and popup window
- Chat history with auto-save and restore - Chat history with auto-save and restore
- Token usage monitoring - Token usage monitoring
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles) - AI personas via agent `system_prompt` — switch personas by switching agents (see [Creating Agents](docs/creating-agents.md))
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries - **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
- **[File Context](docs/file-context.md)** - Attach or link files for better context - **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional) - Automatic syncing with open editor files (optional)
@@ -349,18 +336,16 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │ │ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │
│ │ │ │
│ 1. System Prompt (from Code Completion Settings - FIM variant) │ 1. Editor Context:
2. Project Rules: ├─ File information (language, path)
.qodeassist/rules/completion/*.md Recent project changes (optional, if enabled)
3. Open Files Context (optional, if enabled): └─ Open editor files (optional, if enabled) │
└─ Currently open editor files 2. Code Context:
│ 4. Code Context: │
│ ├─ Code before cursor (prefix) │ │ ├─ Code before cursor (prefix) │
│ └─ Code after cursor (suffix) │ │ └─ Code after cursor (suffix) │
│ │ │ │
│ Final Prompt: FIM_Template(Prefix: SystemPrompt + Rules + OpenFiles + │ Final Request: the agent's TOML [body] template renders prefix/suffix
CodeBefore, into the provider's native FIM fields
│ Suffix: CodeAfter) │
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
``` ```
@@ -375,21 +360,19 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │ │ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │
│ │ │ │
│ 1. System Prompt (from Code Completion Settings - Non-FIM variant) │ 1. Completion Instructions (from the agent's TOML profile)
│ └─ Includes response formatting instructions │ └─ Includes response formatting rules
│ 2. Project Rules: │ 2. Editor Context:
│ └─ .qodeassist/rules/completion/*.md │
│ 3. Open Files Context (optional, if enabled): │
│ └─ Currently open editor files │
│ 4. Code Context: │
│ ├─ File information (language, path) │ │ ├─ File information (language, path) │
│ ├─ Recent project changes (optional, if enabled) │
│ └─ Open editor files (optional, if enabled) │
│ 3. Code Context: │
│ ├─ Code before cursor │ │ ├─ Code before cursor │
│ ├─ <cursor> marker │ │ ├─ <cursor> marker │
│ └─ Code after cursor │ │ └─ Code after cursor │
│ 5. User Message: "Complete the code at cursor position" │
│ │ │ │
│ Final Prompt: [System: SystemPrompt + Rules] │ Final Prompt: [System: Instructions + EditorContext]
│ [User: OpenFiles + Context + CompletionRequest] │ [User: Code around cursor as a completion request] │
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
``` ```
@@ -402,27 +385,22 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ CHAT ASSISTANT │ │ CHAT ASSISTANT │
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Chat Assistant Settings) │ 1. Agent System Prompt (persona, from the agent's TOML profile)
│ 2. Agent Role (optional, from role selector): │ 2. Project Info + Skills (catalog and always-on skills)
└─ Role-specific system prompt (Developer, Reviewer, custom) 3. Tool Definitions (if the agent enables tools)
3. Project Rules: 4. Conversation History:
│ ├─ .qodeassist/rules/common/*.md │ ├─ Previous messages and tool calls/results
.qodeassist/rules/chat/*.md Attachments stay with the message they were sent with
4. File Context (optional): └─ /skill instructions persist for the whole conversation
├─ Attached files (manual) 5. Linked Files + Open Editor Files (if auto-sync enabled):
Linked files (persistent) FRESH snapshot of current file content, re-read on every
└─ Open editor files (if auto-sync enabled) request and placed next to your latest message — never
5. Tool Definitions (if enabled): duplicated into the history
├─ ReadProjectFileByName 6. User Message (+ this turn's attachments and images)
│ ├─ ListProjectFiles │
│ ├─ SearchInProject │
│ └─ GetIssuesList │
│ 6. Conversation History │
│ 7. User Message │
│ │ │ │
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │ Final Prompt: [System: Persona + ProjectInfo + Skills]
│ [History: Previous messages] │ │ [History: Previous messages] │
│ [User: FileContext + UserMessage] │ [User: CurrentFilesSnapshot + UserMessage + Attachments]
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
``` ```
@@ -435,28 +413,26 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ QUICK REFACTORING │ │ QUICK REFACTORING │
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Quick Refactor Settings) │ 1. Agent System Prompt (persona, from the agent's TOML profile)
│ 2. Project Rules: │ 2. Code Context (generated):
│ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/quickrefactor/*.md │
│ 3. Code Context: │
│ ├─ File information (language, path) │ │ ├─ File information (language, path) │
│ ├─ Code before selection (configurable amount) │ │ ├─ Code before selection (configurable amount) │
│ ├─ <selection_start> marker │ │ ├─ <selection_start> marker │
│ ├─ Selected code (or current line) │ │ ├─ Selected code (or current line) │
│ ├─ <selection_end> marker │ │ ├─ <selection_end> marker │
│ ├─ <cursor> marker (position within selection) │ │ ├─ <cursor> marker (position within selection) │
─ Code after selection (configurable amount) │ ─ Code after selection (configurable amount) │
4. Refactor Instruction: └─ Output formatting and indentation rules
│ 3. Refactor Instruction (the user message): │
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │ │ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
│ ├─ Custom Instruction (from library) │ │ ├─ Custom Instruction (from library) │
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │ │ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
│ │ quick_refactor/instructions/*.json │ │ │ quick_refactor/instructions/*.json │
│ └─ Additional Details (optional user input) │ │ └─ Additional Details (optional user input) │
5. Tool Definitions (if enabled) 4. Tool Definitions (if the agent enables tools)
│ │ │ │
│ Final Prompt: [System: SystemPrompt + Rules] │ Final Prompt: [System: Persona + CodeContext + Rules] │
│ [User: Context + Markers + Instruction + Details] │ │ [User: Instruction + Details]
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
``` ```
@@ -464,17 +440,15 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
### Key Points ### Key Points
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure - **System Prompts** live in the agent's TOML profile (`system_prompt`); switch personas by switching agents
- **System Prompts** are configured independently for each feature in Settings - **Linked and open-synced files are always current**: their content is not stored in the conversation — every request re-reads the files and sends a fresh snapshot next to your latest message. Editing a linked file between messages never leaves a stale copy in the context, and changing it does not invalidate the provider's prompt cache for the whole conversation
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only) - **One-time attachments are different**: they are saved with the message they were sent with and stay in the history as sent
- **FIM vs Non-FIM models** for code completion use different System Prompts: - **FIM vs Non-FIM** for code completion is the agent's choice: a FIM agent renders prefix/suffix into native FIM fields, an instruct agent sends a chat-shaped request — pick the agent that matches your model
- FIM models: Direct completion prompt - **Quick Refactor** has its own agent roster, independent from Chat
- Non-FIM models: Prompt includes response formatting instructions
- **Quick Refactor** has its own provider/model configuration, independent from Chat
- **Custom Instructions** provide reusable templates that can be augmented with specific details - **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled - **Tool Calling** is available for Chat and Quick Refactor when the agent enables it; tool rounds per request are limited (configurable in `Settings → Tools`)
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details. See the [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
## QtCreator Version Compatibility ## QtCreator Version Compatibility
@@ -522,7 +496,6 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
- [x] Diff sharing with models - [x] Diff sharing with models
- [x] Tools / function calling (file I/O, build, terminal, diagnostics) - [x] Tools / function calling (file I/O, build, terminal, diagnostics)
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool) - [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
- [x] Project-specific rules (`.qodeassist/rules/`)
- [x] MCP (Model Context Protocol) — QodeAssist as a server - [x] MCP (Model Context Protocol) — QodeAssist as a server
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported) - [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
- [ ] Full project source sharing - [ ] Full project source sharing
@@ -533,7 +506,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository. 1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. 2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers. 3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
@@ -581,6 +554,10 @@ cmake --build .
## For Contributors ## For Contributors
### Adding an agent preset
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
### Code Style ### Code Style
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc - **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
@@ -589,7 +566,7 @@ cmake --build .
### Development Guidelines ### Development Guidelines
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc). For detailed development guidelines and architecture patterns, see [docs/architecture.md](docs/architecture.md) and [docs/target-architecture.md](docs/target-architecture.md).
## License ## License

View File

@@ -32,7 +32,6 @@ public:
void setSuggestionRange(const Utils::Text::Range &range); void setSuggestionRange(const Utils::Text::Range &range);
void clearSuggestionRange(); void clearSuggestionRange();
bool hasSuggestion() const { return m_hasSuggestion; }
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); } void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); } void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }

View File

@@ -1,14 +0,0 @@
add_subdirectory(core)
add_subdirectory(Editor)
# add_subdirectory(serialization)
# add_subdirectory(tasks)
qt_add_library(TaskFlow STATIC)
target_link_libraries(TaskFlow
PUBLIC
TaskFlowCore
TaskFlowEditorplugin
# TaskFlowSerialization
# TaskFlowTasks
)

View File

@@ -1,41 +0,0 @@
qt_add_library(TaskFlowEditor STATIC)
qt_policy(SET QTP0001 NEW)
qt_policy(SET QTP0004 NEW)
qt_add_qml_module(TaskFlowEditor
URI TaskFlow.Editor
VERSION 1.0
DEPENDENCIES QtQuick
RESOURCES
QML_FILES
qml/FlowEditorView.qml
qml/Flow.qml
qml/Task.qml
qml/TaskPort.qml
qml/TaskParameter.qml
qml/TaskConnection.qml
SOURCES
FlowEditor.hpp FlowEditor.cpp
FlowsModel.hpp FlowsModel.cpp
TaskItem.hpp TaskItem.cpp
FlowItem.hpp FlowItem.cpp
TaskModel.hpp TaskModel.cpp
TaskPortItem.hpp TaskPortItem.cpp
TaskPortModel.hpp TaskPortModel.cpp
TaskConnectionsModel.hpp TaskConnectionsModel.cpp
TaskConnectionItem.hpp TaskConnectionItem.cpp
GridBackground.hpp GridBackground.cpp
)
target_link_libraries(TaskFlowEditor
PUBLIC
Qt::Quick
PRIVATE
TaskFlowCore
)
target_include_directories(TaskFlowEditor
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)

View File

@@ -1,105 +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 "FlowEditor.hpp"
namespace QodeAssist::TaskFlow {
FlowEditor::FlowEditor(QQuickItem *parent)
: QQuickItem(parent)
{}
void FlowEditor::initialize()
{
emit availableTaskTypesChanged();
emit availableFlowsChanged();
m_flowsModel = new FlowsModel(m_flowManager, this);
emit flowsModelChanged();
if (m_flowsModel->rowCount() > 0) {
setCurrentFlowIndex(0);
}
// setCurrentFlowId(m_flowManager->flows().begin().value()->flowId());
m_currentFlow = m_flowManager->getFlow();
emit currentFlowChanged();
}
QString FlowEditor::currentFlowId() const
{
return m_currentFlowId;
}
void FlowEditor::setCurrentFlowId(const QString &newCurrentFlowId)
{
if (m_currentFlowId == newCurrentFlowId)
return;
m_currentFlowId = newCurrentFlowId;
emit currentFlowIdChanged();
}
QStringList FlowEditor::availableTaskTypes() const
{
if (m_flowManager)
return m_flowManager->getAvailableTasksTypes();
else {
return {"No flow manager"};
}
}
QStringList FlowEditor::availableFlows() const
{
if (m_flowManager) {
auto flows = m_flowManager->getAvailableFlows();
return flows.size() > 0 ? flows : QStringList{"No flows"};
} else {
return {"No flow manager"};
}
}
void FlowEditor::setFlowManager(FlowManager *newFlowManager)
{
if (m_flowManager == newFlowManager)
return;
m_flowManager = newFlowManager;
initialize();
}
FlowsModel *FlowEditor::flowsModel() const
{
return m_flowsModel;
}
int FlowEditor::currentFlowIndex() const
{
return m_currentFlowIndex;
}
void FlowEditor::setCurrentFlowIndex(int newCurrentFlowIndex)
{
if (m_currentFlowIndex == newCurrentFlowIndex)
return;
m_currentFlowIndex = newCurrentFlowIndex;
emit currentFlowIndexChanged();
}
Flow *FlowEditor::getFlow(const QString &flowName)
{
return m_flowManager->getFlow(flowName);
}
Flow *FlowEditor::getCurrentFlow()
{
return m_flowManager->getFlow(m_currentFlowId);
}
Flow *FlowEditor::currentFlow() const
{
return m_currentFlow;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,71 +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 <QQuickItem>
#include "FlowsModel.hpp"
#include <FlowManager.hpp>
namespace QodeAssist::TaskFlow {
class FlowEditor : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
QString currentFlowId READ currentFlowId WRITE setCurrentFlowId NOTIFY currentFlowIdChanged)
Q_PROPERTY(
QStringList availableTaskTypes READ availableTaskTypes NOTIFY availableTaskTypesChanged)
Q_PROPERTY(QStringList availableFlows READ availableFlows NOTIFY availableFlowsChanged)
Q_PROPERTY(FlowsModel *flowsModel READ flowsModel NOTIFY flowsModelChanged)
Q_PROPERTY(int currentFlowIndex READ currentFlowIndex WRITE setCurrentFlowIndex NOTIFY
currentFlowIndexChanged)
Q_PROPERTY(Flow *currentFlow READ currentFlow NOTIFY currentFlowChanged FINAL)
public:
FlowEditor(QQuickItem *parent = nullptr);
void initialize();
QString currentFlowId() const;
void setCurrentFlowId(const QString &newCurrentFlowId);
QStringList availableTaskTypes() const;
QStringList availableFlows() const;
void setFlowManager(FlowManager *newFlowManager);
FlowsModel *flowsModel() const;
int currentFlowIndex() const;
void setCurrentFlowIndex(int newCurrentFlowIndex);
Q_INVOKABLE Flow *getFlow(const QString &flowName);
Q_INVOKABLE Flow *getCurrentFlow();
Flow *currentFlow() const;
signals:
void currentFlowIdChanged();
void availableTaskTypesChanged();
void availableFlowsChanged();
void flowsModelChanged();
void currentFlowIndexChanged();
void currentFlowChanged();
private:
FlowManager *m_flowManager = nullptr;
QString m_currentFlowId;
FlowsModel *m_flowsModel;
int m_currentFlowIndex;
Flow *m_currentFlow = nullptr;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,94 +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 "FlowItem.hpp"
namespace QodeAssist::TaskFlow {
FlowItem::FlowItem(QQuickItem *parent)
: QQuickItem(parent)
{
connect(this, &QQuickItem::childrenChanged, this, [this]() { updateFlowLayout(); });
}
QString FlowItem::flowId() const
{
if (!m_flow)
return {"no flow"};
return m_flow->flowId();
}
void FlowItem::setFlowId(const QString &newFlowId)
{
if (m_flow->flowId() == newFlowId)
return;
m_flow->setFlowId(newFlowId);
emit flowIdChanged();
}
Flow *FlowItem::flow() const
{
return m_flow;
}
void FlowItem::setFlow(Flow *newFlow)
{
if (m_flow == newFlow)
return;
m_flow = newFlow;
emit flowChanged();
emit flowIdChanged();
qDebug() << "FlowItem::setFlow" << m_flow->flowId() << newFlow;
m_taskModel = new TaskModel(m_flow, this);
m_connectionsModel = new TaskConnectionsModel(m_flow, this);
emit taskModelChanged();
emit connectionsModelChanged();
}
TaskModel *FlowItem::taskModel() const
{
return m_taskModel;
}
TaskConnectionsModel *FlowItem::connectionsModel() const
{
return m_connectionsModel;
}
QVariantList FlowItem::taskItems() const
{
return m_taskItems;
}
void FlowItem::setTaskItems(const QVariantList &newTaskItems)
{
qDebug() << "FlowItem::setTaskItems" << newTaskItems;
if (m_taskItems == newTaskItems)
return;
m_taskItems = newTaskItems;
emit taskItemsChanged();
}
void FlowItem::updateFlowLayout()
{
auto allItems = this->childItems();
for (auto child : allItems) {
if (child->objectName() == QString("TaskItem")) {
qDebug() << "Found TaskItem:" << child;
auto taskItem = qobject_cast<TaskItem *>(child);
m_taskItemsList.insert(taskItem, taskItem->task());
}
if (child->objectName() == QString("TaskConnectionItem")) {
qDebug() << "Found TaskConnectionItem:" << child;
auto connectionItem = qobject_cast<TaskConnectionItem *>(child);
m_taskConnectionsList.insert(connectionItem, connectionItem->connection());
}
}
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,65 +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 <QQuickItem>
#include "TaskConnectionItem.hpp"
#include "TaskConnectionsModel.hpp"
#include "TaskItem.hpp"
#include "TaskModel.hpp"
#include <Flow.hpp>
#include <TaskConnection.hpp>
namespace QodeAssist::TaskFlow {
class FlowItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString flowId READ flowId WRITE setFlowId NOTIFY flowIdChanged)
Q_PROPERTY(Flow *flow READ flow WRITE setFlow NOTIFY flowChanged)
Q_PROPERTY(TaskModel *taskModel READ taskModel NOTIFY taskModelChanged)
Q_PROPERTY(
TaskConnectionsModel *connectionsModel READ connectionsModel NOTIFY connectionsModelChanged)
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
public:
explicit FlowItem(QQuickItem *parent = nullptr);
QString flowId() const;
void setFlowId(const QString &newFlowId);
Flow *flow() const;
void setFlow(Flow *newFlow);
TaskModel *taskModel() const;
TaskConnectionsModel *connectionsModel() const;
QVariantList taskItems() const;
void setTaskItems(const QVariantList &newTaskItems);
void updateFlowLayout();
signals:
void flowIdChanged();
void flowChanged();
void taskModelChanged();
void connectionsModelChanged();
void taskItemsChanged();
private:
Flow *m_flow = nullptr;
TaskModel *m_taskModel = nullptr;
TaskConnectionsModel *m_connectionsModel = nullptr;
QVariantList m_taskItems;
QHash<TaskItem *, BaseTask *> m_taskItemsList;
QHash<TaskConnectionItem *, TaskConnection *> m_taskConnectionsList;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,58 +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 "FlowsModel.hpp"
#include "FlowManager.hpp"
namespace QodeAssist::TaskFlow {
FlowsModel::FlowsModel(FlowManager *flowManager, QObject *parent)
: QAbstractListModel(parent)
, m_flowManager(flowManager)
{
connect(m_flowManager, &FlowManager::flowAdded, this, &FlowsModel::onFlowAdded);
}
int FlowsModel::rowCount(const QModelIndex &parent) const
{
return m_flowManager->flows().size();
}
QVariant FlowsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !m_flowManager || index.row() >= m_flowManager->flows().size())
return QVariant();
const auto flows = m_flowManager->flows().values();
switch (role) {
case FlowRoles::FlowIdRole:
return flows.at(index.row())->flowId();
case FlowRoles::FlowDataRole:
return QVariant::fromValue(flows.at(index.row()));
default:
return QVariant();
}
}
QHash<int, QByteArray> FlowsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[FlowRoles::FlowIdRole] = "flowId";
roles[FlowRoles::FlowDataRole] = "flowData";
return roles;
}
void FlowsModel::onFlowAdded(const QString &flowId)
{
// qDebug() << "FlowsModel::Flow added: " << flowId;
// int newIndex = m_flowManager->flows().size();
// beginInsertRows(QModelIndex(), newIndex, newIndex);
// endInsertRows();
}
void FlowsModel::onFlowRemoved(const QString &flowId) {}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,35 +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 <QAbstractListModel>
#include <QObject>
// #include "tasks/Flow.hpp"
#include <FlowManager.hpp>
namespace QodeAssist::TaskFlow {
class FlowsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum FlowRoles { FlowIdRole = Qt::UserRole, FlowDataRole };
FlowsModel(FlowManager *flowManager, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
public slots:
void onFlowAdded(const QString &flowId);
void onFlowRemoved(const QString &flowId);
private:
FlowManager *m_flowManager;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,83 +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 "GridBackground.hpp"
#include <QPainter>
#include <QPixmap>
#include <QQuickWindow>
#include <QSGSimpleRectNode>
#include <QSGSimpleTextureNode>
namespace QodeAssist::TaskFlow {
GridBackground::GridBackground(QQuickItem *parent)
: QQuickItem(parent)
{
setFlag(QQuickItem::ItemHasContents, true);
}
int GridBackground::gridSize() const
{
return m_gridSize;
}
void GridBackground::setGridSize(int size)
{
if (m_gridSize != size) {
m_gridSize = size;
update();
emit gridSizeChanged();
}
}
QColor GridBackground::gridColor() const
{
return m_gridColor;
}
void GridBackground::setGridColor(const QColor &color)
{
if (m_gridColor != color) {
m_gridColor = color;
update();
emit gridColorChanged();
}
}
QSGNode *GridBackground::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
QSGSimpleTextureNode *node = static_cast<QSGSimpleTextureNode *>(oldNode);
if (!node) {
node = new QSGSimpleTextureNode();
}
QPixmap pixmap(width(), height());
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, false);
QPen pen(m_gridColor);
pen.setWidth(1);
painter.setPen(pen);
painter.setOpacity(this->opacity());
for (int x = 0; x < width(); x += m_gridSize) {
painter.drawLine(x, 0, x, height());
}
for (int y = 0; y < height(); y += m_gridSize) {
painter.drawLine(0, y, width(), y);
}
painter.end();
QSGTexture *texture = window()->createTextureFromImage(pixmap.toImage());
node->setTexture(texture);
node->setRect(boundingRect());
return node;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,42 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QColor>
#include <QPainter>
#include <QQuickItem>
namespace QodeAssist::TaskFlow {
class GridBackground : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged)
public:
explicit GridBackground(QQuickItem *parent = nullptr);
int gridSize() const;
void setGridSize(int size);
QColor gridColor() const;
void setGridColor(const QColor &color);
signals:
void gridSizeChanged();
void gridColorChanged();
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
private:
int m_gridSize = 20;
QColor m_gridColor = QColor(128, 128, 128);
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,157 +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 "TaskConnectionItem.hpp"
#include "TaskItem.hpp"
#include "TaskPortItem.hpp"
#include <QDebug>
namespace QodeAssist::TaskFlow {
TaskConnectionItem::TaskConnectionItem(QQuickItem *parent)
: QQuickItem(parent)
{
setObjectName("TaskConnectionItem");
}
void TaskConnectionItem::setConnection(TaskConnection *connection)
{
if (m_connection == connection)
return;
m_connection = connection;
emit connectionChanged();
calculatePositions();
}
void TaskConnectionItem::updatePositions()
{
// calculatePositions();
}
void TaskConnectionItem::calculatePositions()
{
if (!m_connection) {
return;
}
// Find source task item
QQuickItem *sourceTaskItem = findTaskItem(m_connection->sourceTask());
QQuickItem *targetTaskItem = findTaskItem(m_connection->targetTask());
if (!sourceTaskItem || !targetTaskItem) {
return;
}
// Find port items within tasks
QQuickItem *sourcePortItem = findPortItem(sourceTaskItem, m_connection->sourcePort());
QQuickItem *targetPortItem = findPortItem(targetTaskItem, m_connection->targetPort());
if (!sourcePortItem || !targetPortItem) {
return;
}
// Calculate global positions
QPointF sourceGlobal
= sourcePortItem
->mapToItem(parentItem(), sourcePortItem->width() / 2, sourcePortItem->height() / 2);
QPointF targetGlobal
= targetPortItem
->mapToItem(parentItem(), targetPortItem->width() / 2, targetPortItem->height() / 2);
if (m_startPoint != sourceGlobal) {
m_startPoint = sourceGlobal;
emit startPointChanged();
}
if (m_endPoint != targetGlobal) {
m_endPoint = targetGlobal;
emit endPointChanged();
}
}
QQuickItem *TaskConnectionItem::findTaskItem(BaseTask *task)
{
for (const QVariant &item : m_taskItems) {
QQuickItem *taskItem = qvariant_cast<QQuickItem *>(item);
if (!taskItem)
continue;
QVariant taskProp = taskItem->property("task");
if (taskProp.isValid() && taskProp.value<BaseTask *>() == task) {
return taskItem;
}
}
return nullptr;
}
QQuickItem *TaskConnectionItem::findTaskItemRecursive(QQuickItem *item, BaseTask *task)
{
// Проверяем objectName и task property
if (item->objectName() == "TaskItem") {
QVariant taskProp = item->property("task");
if (taskProp.isValid()) {
BaseTask *itemTask = taskProp.value<BaseTask *>();
if (itemTask == task) {
return item;
}
}
}
// Рекурсивно ищем в детях
auto children = item->childItems();
for (QQuickItem *child : children) {
if (QQuickItem *found = findTaskItemRecursive(child, task)) {
return found;
}
}
return nullptr;
}
QQuickItem *TaskConnectionItem::findPortItem(QQuickItem *taskItem, TaskPort *port)
{
std::function<QQuickItem *(QQuickItem *)> findPortRecursive =
[&](QQuickItem *item) -> QQuickItem * {
// Проверяем objectName и port property
if (item->objectName() == "TaskPortItem") {
QVariant portProp = item->property("port");
if (portProp.isValid()) {
TaskPort *itemPort = portProp.value<TaskPort *>();
if (itemPort == port) {
return item;
}
}
}
// Рекурсивно ищем в детях
for (QQuickItem *child : item->childItems()) {
if (QQuickItem *found = findPortRecursive(child)) {
return found;
}
}
return nullptr;
};
return findPortRecursive(taskItem);
}
QVariantList TaskConnectionItem::taskItems() const
{
return m_taskItems;
}
void TaskConnectionItem::setTaskItems(const QVariantList &newTaskItems)
{
if (m_taskItems == newTaskItems)
return;
m_taskItems = newTaskItems;
emit taskItemsChanged();
calculatePositions();
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,59 +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 "TaskConnection.hpp"
#include <QPointF>
#include <QQuickItem>
namespace QodeAssist::TaskFlow {
class TaskConnectionItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QPointF startPoint READ startPoint NOTIFY startPointChanged)
Q_PROPERTY(QPointF endPoint READ endPoint NOTIFY endPointChanged)
Q_PROPERTY(
TaskConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
public:
TaskConnectionItem(QQuickItem *parent = nullptr);
QPointF startPoint() const { return m_startPoint; }
QPointF endPoint() const { return m_endPoint; }
TaskConnection *connection() const { return m_connection; }
void setConnection(TaskConnection *connection);
Q_INVOKABLE void updatePositions();
QVariantList taskItems() const;
void setTaskItems(const QVariantList &newTaskItems);
signals:
void startPointChanged();
void endPointChanged();
void connectionChanged();
void taskItemsChanged();
private:
void calculatePositions();
QQuickItem *findTaskItem(BaseTask *task);
QQuickItem *findTaskItemRecursive(QQuickItem *item, BaseTask *task);
QQuickItem *findPortItem(QQuickItem *taskItem, TaskPort *port);
private:
TaskConnection *m_connection = nullptr;
QPointF m_startPoint;
QPointF m_endPoint;
QVariantList m_taskItems;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,33 +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 "TaskConnectionsModel.hpp"
namespace QodeAssist::TaskFlow {
TaskConnectionsModel::TaskConnectionsModel(Flow *flow, QObject *parent)
: QAbstractListModel(parent)
, m_flow(flow)
{}
int TaskConnectionsModel::rowCount(const QModelIndex &parent) const
{
return m_flow->connections().size();
}
QVariant TaskConnectionsModel::data(const QModelIndex &index, int role) const
{
if (role == TaskConnectionsRoles::TaskConnectionsRole)
return QVariant::fromValue(m_flow->connections().at(index.row()));
return QVariant();
}
QHash<int, QByteArray> TaskConnectionsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskConnectionsRoles::TaskConnectionsRole] = "connectionData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,29 +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 <QAbstractListModel>
#include <QObject>
#include <Flow.hpp>
namespace QodeAssist::TaskFlow {
class TaskConnectionsModel : public QAbstractListModel
{
public:
enum TaskConnectionsRoles { TaskConnectionsRole = Qt::UserRole };
explicit TaskConnectionsModel(Flow *flow, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
Flow *m_flow;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,73 +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 "TaskItem.hpp"
namespace QodeAssist::TaskFlow {
TaskItem::TaskItem(QQuickItem *parent)
: QQuickItem(parent)
{
setObjectName("TaskItem");
}
QString TaskItem::taskId() const
{
return m_taskId;
}
void TaskItem::setTaskId(const QString &newTaskId)
{
if (m_taskId == newTaskId)
return;
m_taskId = newTaskId;
emit taskIdChanged();
}
QString TaskItem::taskType() const
{
return m_task ? m_task->taskType() : QString();
}
BaseTask *TaskItem::task() const
{
return m_task;
}
void TaskItem::setTask(BaseTask *newTask)
{
if (m_task == newTask)
return;
m_task = newTask;
if (m_task) {
m_taskId = m_task->taskId();
// Обновляем модели портов
m_inputPorts = new TaskPortModel(m_task->getInputPorts(), this);
m_outputPorts = new TaskPortModel(m_task->getOutputPorts(), this);
} else {
m_inputPorts = nullptr;
m_outputPorts = nullptr;
}
emit taskChanged();
emit inputPortsChanged();
emit outputPortsChanged();
emit taskIdChanged();
emit taskTypeChanged();
}
TaskPortModel *TaskItem::inputPorts() const
{
return m_inputPorts;
}
TaskPortModel *TaskItem::outputPorts() const
{
return m_outputPorts;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,53 +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 <QQuickItem>
#include "TaskPortModel.hpp"
#include <BaseTask.hpp>
#include <TaskPort.hpp>
namespace QodeAssist::TaskFlow {
class TaskItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString taskId READ taskId WRITE setTaskId NOTIFY taskIdChanged)
Q_PROPERTY(QString taskType READ taskType NOTIFY taskTypeChanged)
Q_PROPERTY(BaseTask *task READ task WRITE setTask NOTIFY taskChanged)
Q_PROPERTY(TaskPortModel *inputPorts READ inputPorts NOTIFY inputPortsChanged)
Q_PROPERTY(TaskPortModel *outputPorts READ outputPorts NOTIFY outputPortsChanged)
public:
TaskItem(QQuickItem *parent = nullptr);
QString taskId() const;
void setTaskId(const QString &newTaskId);
QString taskType() const;
BaseTask *task() const;
void setTask(BaseTask *newTask);
TaskPortModel *inputPorts() const;
TaskPortModel *outputPorts() const;
signals:
void taskIdChanged();
void taskTypeChanged();
void taskChanged();
void inputPortsChanged();
void outputPortsChanged();
private:
QString m_taskId;
BaseTask *m_task = nullptr;
TaskPortModel *m_inputPorts = nullptr;
TaskPortModel *m_outputPorts = nullptr;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,44 +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 "TaskModel.hpp"
namespace QodeAssist::TaskFlow {
TaskModel::TaskModel(Flow *flow, QObject *parent)
: QAbstractListModel(parent)
, m_flow(flow)
{}
int TaskModel::rowCount(const QModelIndex &parent) const
{
return m_flow->tasks().size();
}
QVariant TaskModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !m_flow || index.row() >= m_flow->tasks().size())
return QVariant();
const auto &task = m_flow->tasks().values();
switch (role) {
case TaskRoles::TaskIdRole:
return task.at(index.row())->taskId();
case TaskRoles::TaskDataRole:
return QVariant::fromValue(task.at(index.row()));
default:
return QVariant();
}
}
QHash<int, QByteArray> TaskModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskRoles::TaskIdRole] = "taskId";
roles[TaskRoles::TaskDataRole] = "taskData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,29 +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 <QAbstractListModel>
#include <Flow.hpp>
namespace QodeAssist::TaskFlow {
class TaskModel : public QAbstractListModel
{
Q_OBJECT
public:
enum TaskRoles { TaskIdRole = Qt::UserRole, TaskDataRole };
TaskModel(Flow *flow, QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
Flow *m_flow;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,33 +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 "TaskPortItem.hpp"
namespace QodeAssist::TaskFlow {
TaskPortItem::TaskPortItem(QQuickItem *parent)
: QQuickItem(parent)
{
setObjectName("TaskPortItem");
}
TaskPort *TaskPortItem::port() const
{
return m_port;
}
void TaskPortItem::setPort(TaskPort *newPort)
{
if (m_port == newPort)
return;
m_port = newPort;
emit portChanged();
}
QString TaskPortItem::name() const
{
return m_port->name();
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,35 +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 <TaskPort.hpp>
#include <QQuickItem>
namespace QodeAssist::TaskFlow {
class TaskPortItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(TaskPort *port READ port WRITE setPort NOTIFY portChanged)
Q_PROPERTY(QString name READ name CONSTANT)
public:
TaskPortItem(QQuickItem *parent = nullptr);
TaskPort *port() const;
void setPort(TaskPort *newPort);
QString name() const;
signals:
void portChanged();
private:
TaskPort *m_port = nullptr;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,43 +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 "TaskPortModel.hpp"
#include "TaskPort.hpp"
namespace QodeAssist::TaskFlow {
TaskPortModel::TaskPortModel(const QList<TaskPort *> &ports, QObject *parent)
: QAbstractListModel(parent)
, m_ports(ports)
{}
int TaskPortModel::rowCount(const QModelIndex &parent) const
{
return m_ports.size();
}
QVariant TaskPortModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_ports.size())
return QVariant();
switch (role) {
case TaskPortRoles::TaskPortNameRole:
return m_ports.at(index.row())->name();
case TaskPortRoles::TaskPortDataRole:
return QVariant::fromValue(m_ports.at(index.row()));
default:
return QVariant();
}
}
QHash<int, QByteArray> TaskPortModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskPortRoles::TaskPortNameRole] = "taskPortName";
roles[TaskPortRoles::TaskPortDataRole] = "taskPortData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,29 +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 <QAbstractListModel>
#include <BaseTask.hpp>
namespace QodeAssist::TaskFlow {
class TaskPortModel : public QAbstractListModel
{
Q_OBJECT
public:
enum TaskPortRoles { TaskPortNameRole = Qt::UserRole, TaskPortDataRole };
TaskPortModel(const QList<TaskPort *> &ports, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
QList<TaskPort *> m_ports;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,138 +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
import QtQuick
import TaskFlow.Editor
FlowItem {
id: root
Repeater {
id: tasks
model: root.taskModel
delegate: Task {
// task: taskData
}
}
Repeater {
id: connections
model: root.taskModel
delegate: TaskConnection {
// task: taskData
}
}
// property var qtaskItems: []
// // Flow container background
// Rectangle {
// anchors.fill: parent
// color: palette.alternateBase
// border.color: palette.mid
// border.width: 2
// radius: 8
// // Flow header
// Rectangle {
// id: flowHeader
// anchors.top: parent.top
// anchors.left: parent.left
// anchors.right: parent.right
// height: 40
// color: palette.button
// radius: 6
// Rectangle {
// anchors.bottom: parent.bottom
// anchors.left: parent.left
// anchors.right: parent.right
// height: parent.radius
// color: parent.color
// }
// Text {
// anchors.centerIn: parent
// text: root.flowId
// color: palette.buttonText
// font.pixelSize: 14
// font.bold: true
// }
// }
// // // Tasks container
// // Row {
// // id: tasksRow
// // anchors.top: flowHeader.bottom
// // anchors.left: parent.left
// // anchors.margins: 25
// // anchors.topMargin: 25
// // objectName: "FlowTaskRow"
// // spacing: 40
// // Repeater {
// // model: root.taskModel
// // delegate: Task {
// // task: taskData
// // }
// // onItemAdded: function(index, item){
// // console.log("task added", index, item)
// // qtaskItems.push(item)
// // root.insertTaskItem(index, item)
// // }
// // onItemRemoved: function(index, item){
// // console.log("task added", index, item)
// // var idx = qtaskItems.indexOf(item)
// // if (idx !== -1) qtaskItems.splice(idx, 1)
// // }
// // }
// // }
// // Repeater {
// // model: root.connectionsModel
// // delegate: TaskConnection {
// // connection: connectionData
// // }
// // }
// }
// // Flow info tooltip
// Rectangle {
// id: infoTooltip
// anchors.top: parent.bottom
// anchors.left: parent.left
// anchors.topMargin: 5
// width: infoText.width + 20
// height: infoText.height + 10
// color: palette.base
// border.color: palette.shadow
// border.width: 1
// radius: 4
// visible: false
// Text {
// id: infoText
// anchors.centerIn: parent
// text: "Tasks: " + (root.taskModel ? root.taskModel.rowCount() : 0)
// color: palette.text
// font.pixelSize: 10
// }
// }
// MouseArea {
// anchors.fill: parent
// hoverEnabled: true
// onEntered: infoTooltip.visible = true
// onExited: infoTooltip.visible = false
// }
}

View File

@@ -1,125 +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
import QtQuick
import QtQuick.Controls
import TaskFlow.Editor
FlowEditor {
id: root
width: 1200
height: 800
property SystemPalette sysPalette: SystemPalette {
colorGroup: SystemPalette.Active
}
palette {
window: sysPalette.window
windowText: sysPalette.windowText
base: sysPalette.base
alternateBase: sysPalette.alternateBase
text: sysPalette.text
button: sysPalette.button
buttonText: sysPalette.buttonText
highlight: sysPalette.highlight
highlightedText: sysPalette.highlightedText
light: sysPalette.light
mid: sysPalette.mid
dark: sysPalette.dark
shadow: sysPalette.shadow
brightText: sysPalette.brightText
}
// Background with grid pattern
Rectangle {
anchors.fill: parent
color: palette.window
// Grid pattern using C++ implementation
GridBackground {
anchors.fill: parent
gridSize: 20
gridColor: palette.mid
opacity: 0.3
}
}
// Header panel
Rectangle {
id: headerPanel
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 60
color: palette.base
border.color: palette.mid
border.width: 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 20
spacing: 20
Text {
text: "Flow Editor"
color: palette.windowText
font.pixelSize: 18
font.bold: true
}
Rectangle {
width: 2
height: 30
color: palette.mid
}
Text {
text: "Flow:"
color: palette.text
font.pixelSize: 14
}
ComboBox {
id: flowComboBox
model: root.flowsModel
textRole: "flowId"
currentIndex: root.currentFlowIndex
onActivated: {
root.currentFlowIndex = currentIndex
}
}
Text {
text: "Available Tasks: " + root.availableTaskTypes.join(", ")
color: palette.text
font.pixelSize: 12
}
}
}
// Main flow area
ScrollView {
id: scrollView
anchors.top: headerPanel.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
contentWidth: flow.width
contentHeight: flow.height
Flow {
id: flow
// flow: root.currentFlow
width: Math.max(root.width, 0)
height: Math.min(root.height, 0)
}
}
}

View File

@@ -1,214 +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
import QtQuick
import TaskFlow.Editor
TaskItem{
id: root
width: 280
height: Math.max(200, contentColumn.height + 40)
DragHandler {
id: dragHandler
target: root
onActiveChanged: {
if (active) {
root.z = 1000; // Поднять над остальными
} else {
root.z = 0;
}
}
}
// Task node background
Rectangle {
anchors.fill: parent
color: palette.window
border.color: palette.shadow
border.width: 1
radius: 6
// Task header
Rectangle {
id: taskHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 40
color: palette.button
radius: 6
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: parent.radius
color: parent.color
}
Text {
anchors.centerIn: parent
// text: root.taskType
color: palette.buttonText
font.pixelSize: 14
font.bold: true
}
}
// Task content
Column {
id: contentColumn
anchors.top: taskHeader.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 10
spacing: 8
// Task ID
Text {
text: "ID: " + root.taskId
color: palette.text
font.pixelSize: 11
width: parent.width
elide: Text.ElideRight
}
// Parameters section
Item {
width: parent.width
height: paramColumn.height
// visible: root.parameters && root.parameters.rowCount() > 0
Column {
id: paramColumn
width: parent.width
spacing: 6
Text {
text: "Parameters:"
color: palette.text
font.pixelSize: 10
font.bold: true
}
Repeater {
model: root.parameters
delegate: Rectangle {
width: parent.width
height: 24
color: palette.base
radius: 4
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
spacing: 6
Text {
text: paramKey + ":"
color: palette.text
font.pixelSize: 9
font.bold: true
}
Text {
text: paramValue
color: palette.windowText
font.pixelSize: 9
width: Math.min(150, implicitWidth)
elide: Text.ElideRight
}
}
}
}
}
}
}
}
// Input ports section (left side)
Column {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: -8
spacing: 6
// visible: root.inputPorts && root.inputPorts.rowCount() > 0
// Input label
Text {
text: "IN"
color: palette.highlight
font.pixelSize: 10
font.bold: true
anchors.left: parent.left
anchors.leftMargin: -20
}
// Repeater {
// model: root.inputPorts
// delegate: Row {
// spacing: 6
// Text {
// text: taskPortName
// color: palette.text
// font.pixelSize: 9
// anchors.verticalCenter: parent.verticalCenter
// horizontalAlignment: Text.AlignRight
// width: 60
// elide: Text.ElideLeft
// }
// TaskPort {
// port: taskPortData
// isInput: true
// }
// }
// }
}
// Output ports section (right side)
Column {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: -10
spacing: 8
// visible: root.outputPorts && root.outputPorts.rowCount() > 0
// Output label
Text {
text: "OUT"
color: palette.highlight
font.pixelSize: 10
font.bold: true
anchors.right: parent.right
anchors.rightMargin: -24
}
// Repeater {
// model: root.outputPorts
// delegate: Row {
// spacing: 6
// TaskPort {
// port: taskPortData
// isInput: false
// }
// Text {
// text: taskPortName
// color: palette.text
// font.pixelSize: 9
// anchors.verticalCenter: parent.verticalCenter
// width: 60
// elide: Text.ElideRight
// }
// }
// }
}
}

View File

@@ -1,72 +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
import QtQuick
import QtQuick.Shapes
import TaskFlow.Editor
TaskConnectionItem {
id: root
property color connectionColor: "red"
Rectangle {
width: 10
height: 10
radius: width / 2
color: "blue"
}
// width: Math.abs(endPoint.x - startPoint.x) + 40
// height: Math.abs(endPoint.y - startPoint.y) + 40
// x: Math.min(startPoint.x, endPoint.x) - 20
// y: Math.min(startPoint.y, endPoint.y) - 20
// Shape {
// anchors.fill: parent
// ShapePath {
// strokeWidth: 2
// strokeColor: connectionColor
// fillColor: "transparent"
// property point localStart: Qt.point(
// root.startPoint.x - root.x,
// root.startPoint.y - root.y
// )
// property point localEnd: Qt.point(
// root.endPoint.x - root.x,
// root.endPoint.y - root.y
// )
// // Bezier curve
// property real controlOffset: Math.max(50, Math.abs(localEnd.x - localStart.x) * 0.4)
// startX: localStart.x
// startY: localStart.y
// PathCubic {
// x: parent.localEnd.x
// y: parent.localEnd.y
// control1X: parent.localStart.x + parent.controlOffset
// control1Y: parent.localStart.y
// control2X: parent.localEnd.x - parent.controlOffset
// control2Y: parent.localEnd.y
// }
// }
// // Arrow head
// Rectangle {
// width: 8
// height: 8
// color: connectionColor
// rotation: 45
// x: root.endPoint.x - root.x - 4
// y: root.endPoint.y - root.y - 4
// }
// }
// // Update positions when tasks might have moved
// Component.onCompleted: updatePositions()
}

View File

@@ -1,10 +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
import QtQuick
import TaskFlow.Editor
Item {
}

View File

@@ -1,67 +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
import QtQuick
import TaskFlow.Editor
TaskPortItem {
id: root
property bool isInput: true
width: 20
height: 20
// Port circle
Rectangle {
id: portCircle
anchors.centerIn: parent
width: 16
height: 16
radius: 8
color: getPortColor()
border.color: palette.windowText
border.width: 1
// Inner circle for connected state simulation
Rectangle {
anchors.centerIn: parent
width: 8
height: 8
radius: 4
color: root.port ? palette.windowText : "transparent"
visible: root.port !== null
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
portCircle.scale = 1.3
portCircle.border.width = 2
}
onExited: {
portCircle.scale = 1.0
portCircle.border.width = 1
}
}
function getPortColor() {
if (!root.port) return palette.mid
// Different colors for input/output using system palette
if (root.isInput) {
return palette.highlight // System highlight color for inputs
} else {
return Qt.lighter(palette.highlight, 1.3) // Lighter highlight for outputs
}
}
Behavior on scale {
NumberAnimation { duration: 100 }
}
}

View File

@@ -1,102 +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 "BaseTask.hpp"
#include "TaskPort.hpp"
#include <QUuid>
#include <QtConcurrent>
namespace QodeAssist::TaskFlow {
BaseTask::BaseTask(QObject *parent)
: QObject(parent)
, m_taskId("unknown" + QUuid::createUuid().toString())
{}
BaseTask::~BaseTask()
{
qDeleteAll(m_inputs);
qDeleteAll(m_outputs);
}
QString BaseTask::taskId() const
{
return m_taskId;
}
void BaseTask::setTaskId(const QString &taskId)
{
m_taskId = taskId;
}
QString BaseTask::taskType() const
{
return QString(metaObject()->className()).split("::").last();
}
void BaseTask::addInputPort(const QString &name)
{
QMutexLocker locker(&m_tasksMutex);
m_inputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
}
void BaseTask::addOutputPort(const QString &name)
{
QMutexLocker locker(&m_tasksMutex);
m_outputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
}
TaskPort *BaseTask::inputPort(const QString &name) const
{
QMutexLocker locker(&m_tasksMutex);
auto it = std::find_if(m_inputs.begin(), m_inputs.end(), [&name](const TaskPort *port) {
return port->name() == name;
});
return (it != m_inputs.end()) ? *it : nullptr;
}
TaskPort *BaseTask::outputPort(const QString &name) const
{
QMutexLocker locker(&m_tasksMutex);
auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&name](const TaskPort *port) {
return port->name() == name;
});
return (it != m_outputs.end()) ? *it : nullptr;
}
QList<TaskPort *> BaseTask::getInputPorts() const
{
QMutexLocker locker(&m_tasksMutex);
return m_inputs;
}
QList<TaskPort *> BaseTask::getOutputPorts() const
{
QMutexLocker locker(&m_tasksMutex);
return m_outputs;
}
QFuture<TaskState> BaseTask::executeAsync()
{
return QtConcurrent::task([this]() -> TaskState { return execute(); }).spawn();
}
QString BaseTask::taskStateAsString(TaskState state)
{
switch (state) {
case TaskState::Success:
return "Success";
case TaskState::Failed:
return "Failed";
case TaskState::Cancelled:
return "Cancelled";
}
return "Unknown";
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,53 +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 <QFuture>
#include <QMetaType>
#include <QMutex>
#include <QObject>
namespace QodeAssist::TaskFlow {
class TaskPort;
enum class TaskState { Success, Failed, Cancelled };
class BaseTask : public QObject
{
Q_OBJECT
public:
explicit BaseTask(QObject *parent = nullptr);
virtual ~BaseTask();
QString taskId() const;
void setTaskId(const QString &taskId);
QString taskType() const;
void addInputPort(const QString &name);
void addOutputPort(const QString &name);
TaskPort *inputPort(const QString &name) const;
TaskPort *outputPort(const QString &name) const;
QList<TaskPort *> getInputPorts() const;
QList<TaskPort *> getOutputPorts() const;
virtual TaskState execute() = 0;
static QString taskStateAsString(TaskState state);
protected:
QFuture<TaskState> executeAsync();
private:
QString m_taskId;
QList<TaskPort *> m_inputs;
QList<TaskPort *> m_outputs;
mutable QMutex m_tasksMutex;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,22 +0,0 @@
qt_add_library(TaskFlowCore STATIC
BaseTask.hpp BaseTask.cpp
TaskConnection.hpp TaskConnection.cpp
Flow.hpp Flow.cpp
TaskPort.hpp TaskPort.cpp
TaskRegistry.hpp TaskRegistry.cpp
FlowManager.hpp FlowManager.cpp
FlowRegistry.hpp FlowRegistry.cpp
)
target_link_libraries(TaskFlowCore
PUBLIC
Qt::Core
Qt::Concurrent
PRIVATE
QodeAssistLogger
)
target_include_directories(TaskFlowCore
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -1,340 +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 "Flow.hpp"
#include "TaskPort.hpp"
#include <QUuid>
#include <QtConcurrent>
namespace QodeAssist::TaskFlow {
Flow::Flow(QObject *parent)
: QObject(parent)
, m_flowId("flow_" + QUuid::createUuid().toString())
{}
Flow::~Flow()
{
QMutexLocker locker(&m_flowMutex);
qDeleteAll(m_connections);
qDeleteAll(m_tasks);
}
QString Flow::flowId() const
{
return m_flowId;
}
void Flow::setFlowId(const QString &flowId)
{
if (m_flowId != flowId) {
m_flowId = flowId;
}
}
void Flow::addTask(BaseTask *task)
{
if (!task) {
return;
}
QMutexLocker locker(&m_flowMutex);
QString taskId = task->taskId();
if (m_tasks.contains(taskId)) {
qWarning() << "Flow::addTask - Task with ID" << taskId << "already exists";
return;
}
m_tasks.insert(taskId, task);
task->setParent(this);
emit taskAdded(taskId);
}
void Flow::removeTask(const QString &taskId)
{
QMutexLocker locker(&m_flowMutex);
BaseTask *task = m_tasks.value(taskId);
if (!task) {
return;
}
auto it = m_connections.begin();
while (it != m_connections.end()) {
TaskConnection *connection = *it;
if (connection->sourceTask() == task || connection->targetTask() == task) {
it = m_connections.erase(it);
emit connectionRemoved(connection);
delete connection;
} else {
++it;
}
}
m_tasks.remove(taskId);
emit taskRemoved(taskId);
delete task;
}
void Flow::removeTask(BaseTask *task)
{
if (!task) {
return;
}
removeTask(task->taskId());
}
BaseTask *Flow::getTask(const QString &taskId) const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks.value(taskId);
}
bool Flow::hasTask(const QString &taskId) const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks.contains(taskId);
}
QHash<QString, BaseTask *> Flow::tasks() const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks;
}
TaskConnection *Flow::addConnection(TaskPort *sourcePort, TaskPort *targetPort)
{
if (!sourcePort || !targetPort) {
qWarning() << "Flow::addConnection - Invalid ports";
return nullptr;
}
// Verify ports belong to tasks in this flow
BaseTask *sourceTask = qobject_cast<BaseTask *>(sourcePort->parent());
BaseTask *targetTask = qobject_cast<BaseTask *>(targetPort->parent());
if (!sourceTask || !targetTask) {
qWarning() << "Flow::addConnection - Ports don't belong to valid tasks";
return nullptr;
}
QMutexLocker locker(&m_flowMutex);
if (!m_tasks.contains(sourceTask->taskId()) || !m_tasks.contains(targetTask->taskId())) {
qWarning() << "Flow::addConnection - Tasks not in this flow";
return nullptr;
}
for (TaskConnection *existingConnection : m_connections) {
if (existingConnection->sourcePort() == sourcePort
&& existingConnection->targetPort() == targetPort) {
qWarning() << "Flow::addConnection - Connection already exists";
return existingConnection;
}
}
TaskConnection *connection = new TaskConnection(sourcePort, targetPort, this);
m_connections.append(connection);
emit connectionAdded(connection);
return connection;
}
void Flow::removeConnection(TaskConnection *connection)
{
if (!connection) {
return;
}
QMutexLocker locker(&m_flowMutex);
if (m_connections.removeOne(connection)) {
emit connectionRemoved(connection);
delete connection;
}
}
QList<TaskConnection *> Flow::connections() const
{
QMutexLocker locker(&m_flowMutex);
return m_connections;
}
QFuture<FlowState> Flow::executeAsync()
{
return QtConcurrent::run([this]() { return execute(); });
}
FlowState Flow::execute()
{
emit executionStarted();
if (!isValid()) {
emit executionFinished(FlowState::Failed);
return FlowState::Failed;
}
if (hasCircularDependencies()) {
qWarning() << "Flow::execute - Circular dependencies detected";
emit executionFinished(FlowState::Failed);
return FlowState::Failed;
}
QList<BaseTask *> executionOrder = getExecutionOrder();
for (BaseTask *task : executionOrder) {
TaskState taskResult = task->execute();
if (taskResult == TaskState::Failed) {
qWarning() << "Flow::execute - Task" << task->taskId() << "failed";
emit executionFinished(FlowState::Failed);
return FlowState::Failed;
}
if (taskResult == TaskState::Cancelled) {
qWarning() << "Flow::execute - Task" << task->taskId() << "cancelled";
emit executionFinished(FlowState::Cancelled);
return FlowState::Cancelled;
}
}
emit executionFinished(FlowState::Success);
return FlowState::Success;
}
bool Flow::isValid() const
{
QMutexLocker locker(&m_flowMutex);
// Check all connections are valid
for (TaskConnection *connection : m_connections) {
if (!connection->isValid()) {
return false;
}
}
return true;
}
bool Flow::hasCircularDependencies() const
{
return detectCircularDependencies();
}
QString Flow::flowStateAsString(FlowState state)
{
switch (state) {
case FlowState::Success:
return "Success";
case FlowState::Failed:
return "Failed";
case FlowState::Cancelled:
return "Cancelled";
}
return "Unknown";
}
QStringList Flow::getTaskIds() const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks.keys();
}
QList<BaseTask *> Flow::getExecutionOrder() const
{
QMutexLocker locker(&m_flowMutex);
QList<BaseTask *> result;
QSet<BaseTask *> visited;
QList<BaseTask *> allTasks = m_tasks.values();
std::function<void(BaseTask *)> visit = [&](BaseTask *task) {
if (visited.contains(task)) {
return;
}
visited.insert(task);
QList<BaseTask *> dependencies = getTaskDependencies(task);
for (BaseTask *dependency : dependencies) {
visit(dependency);
}
result.append(task);
};
for (BaseTask *task : allTasks) {
visit(task);
}
return result;
}
bool Flow::detectCircularDependencies() const
{
QMutexLocker locker(&m_flowMutex);
QSet<BaseTask *> visited;
QSet<BaseTask *> recursionStack;
bool hasCycle = false;
for (BaseTask *task : m_tasks.values()) {
if (!visited.contains(task)) {
visitTask(task, visited, recursionStack, hasCycle);
if (hasCycle) {
return true;
}
}
}
return false;
}
void Flow::visitTask(
BaseTask *task, QSet<BaseTask *> &visited, QSet<BaseTask *> &recursionStack, bool &hasCycle) const
{
if (hasCycle) {
return;
}
visited.insert(task);
recursionStack.insert(task);
for (TaskConnection *connection : m_connections) {
if (connection->sourceTask() == task) {
BaseTask *dependentTask = connection->targetTask();
if (recursionStack.contains(dependentTask)) {
hasCycle = true;
return;
}
if (!visited.contains(dependentTask)) {
visitTask(dependentTask, visited, recursionStack, hasCycle);
}
}
}
recursionStack.remove(task);
}
QList<BaseTask *> Flow::getTaskDependencies(BaseTask *task) const
{
QList<BaseTask *> dependencies;
for (TaskConnection *connection : m_connections) {
if (connection->targetTask() == task) {
BaseTask *dependencyTask = connection->sourceTask();
if (!dependencies.contains(dependencyTask)) {
dependencies.append(dependencyTask);
}
}
}
return dependencies;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,80 +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 <QFuture>
#include <QHash>
#include <QList>
#include <QMetaType>
#include <QMutex>
#include <QObject>
#include "BaseTask.hpp"
#include "TaskConnection.hpp"
namespace QodeAssist::TaskFlow {
enum class FlowState { Success, Failed, Cancelled };
class Flow : public QObject
{
Q_OBJECT
public:
explicit Flow(QObject *parent = nullptr);
~Flow() override;
QString flowId() const;
void setFlowId(const QString &flowId);
void addTask(BaseTask *task);
void removeTask(const QString &taskId);
void removeTask(BaseTask *task);
BaseTask *getTask(const QString &taskId) const;
bool hasTask(const QString &taskId) const;
QHash<QString, BaseTask *> tasks() const;
TaskConnection *addConnection(TaskPort *sourcePort, TaskPort *targetPort);
void removeConnection(TaskConnection *connection);
QList<TaskConnection *> connections() const;
QFuture<FlowState> executeAsync();
virtual FlowState execute();
bool isValid() const;
bool hasCircularDependencies() const;
static QString flowStateAsString(FlowState state);
QStringList getTaskIds() const;
signals:
void taskAdded(const QString &taskId);
void taskRemoved(const QString &taskId);
void connectionAdded(QodeAssist::TaskFlow::TaskConnection *connection);
void connectionRemoved(QodeAssist::TaskFlow::TaskConnection *connection);
void executionStarted();
void executionFinished(FlowState result);
private:
QString m_flowId;
QHash<QString, BaseTask *> m_tasks;
QList<TaskConnection *> m_connections;
mutable QMutex m_flowMutex;
QList<BaseTask *> getExecutionOrder() const;
bool detectCircularDependencies() const;
void visitTask(
BaseTask *task,
QSet<BaseTask *> &visited,
QSet<BaseTask *> &recursionStack,
bool &hasCycle) const;
QList<BaseTask *> getTaskDependencies(BaseTask *task) const;
};
} // namespace QodeAssist::TaskFlow
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::Flow *)
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::FlowState)

View File

@@ -1,97 +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 "FlowManager.hpp"
#include <Logger.hpp>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include "FlowRegistry.hpp"
#include "TaskRegistry.hpp"
namespace QodeAssist::TaskFlow {
FlowManager::FlowManager(QObject *parent)
: QObject(parent)
, m_taskRegistry(new TaskRegistry(this))
, m_flowRegistry(new FlowRegistry(this))
{
LOG_MESSAGE("FlowManager created");
}
FlowManager::~FlowManager()
{
clear();
}
// Flow *FlowManager::createFlow(const QString &flowId)
// {
// Flow *flow = new Flow(flowId, m_taskRegistry, this);
// if (!m_flows.contains(flow->flowId())) {
// m_flows.insert(flowId, flow);
// } else {
// LOG_MESSAGE(
// QString("FlowManager::createFlow - flow with id %1 already exists").arg(flow->flowId()));
// }
// return flow;
// }
void FlowManager::addFlow(Flow *flow)
{
qDebug() << "FlowManager::addFlow" << flow->flowId();
if (!m_flows.contains(flow->flowId())) {
m_flows.insert(flow->flowId(), flow);
flow->setParent(this);
emit flowAdded(flow->flowId());
} else {
LOG_MESSAGE(
QString("FlowManager::addFlow - flow with id %1 already exists").arg(flow->flowId()));
}
}
void FlowManager::clear()
{
LOG_MESSAGE(QString("FlowManager::clear - removing %1 flows").arg(m_flows.size()));
qDeleteAll(m_flows);
m_flows.clear();
}
QStringList FlowManager::getAvailableTasksTypes()
{
return m_taskRegistry->getAvailableTypes();
}
QStringList FlowManager::getAvailableFlows()
{
return m_flowRegistry->getAvailableTypes();
}
QHash<QString, Flow *> FlowManager::flows() const
{
return m_flows;
}
TaskRegistry *FlowManager::taskRegistry() const
{
return m_taskRegistry;
}
FlowRegistry *FlowManager::flowRegistry() const
{
return m_flowRegistry;
}
Flow *FlowManager::getFlow(const QString &flowId) const
{
// if (flowId.isEmpty()) {
// return m_flows.begin().value();
// }
// return m_flows.value(flowId, nullptr);
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,54 +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 <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QString>
#include "Flow.hpp"
namespace QodeAssist::TaskFlow {
class TaskRegistry;
class FlowRegistry;
class FlowManager : public QObject
{
Q_OBJECT
public:
explicit FlowManager(QObject *parent = nullptr);
~FlowManager() override;
// Flow *createFlow(const QString &flowId);
void addFlow(Flow *flow);
void clear();
QStringList getAvailableTasksTypes();
QStringList getAvailableFlows();
QHash<QString, Flow *> flows() const;
TaskRegistry *taskRegistry() const;
FlowRegistry *flowRegistry() const;
Flow *getFlow(const QString &flowId = {}) const;
signals:
void flowAdded(const QString &flowId);
void flowRemoved(const QString &flowId);
private:
QHash<QString, Flow *> m_flows;
TaskRegistry *m_taskRegistry;
FlowRegistry *m_flowRegistry;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,47 +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 "FlowRegistry.hpp"
#include "Logger.hpp"
namespace QodeAssist::TaskFlow {
FlowRegistry::FlowRegistry(QObject *parent)
: QObject(parent)
{}
void FlowRegistry::registerFlow(const QString &flowType, FlowCreator creator)
{
m_flowCreators[flowType] = creator;
LOG_MESSAGE(QString("FlowRegistry: Registered flow type '%1'").arg(flowType));
}
Flow *FlowRegistry::createFlow(const QString &flowType, FlowManager *flowManager) const
{
LOG_MESSAGE(QString("Trying to create flow: %1").arg(flowType));
if (m_flowCreators.contains(flowType)) {
LOG_MESSAGE(QString("Found creator for flow type: %1").arg(flowType));
try {
Flow *flow = m_flowCreators[flowType](flowManager);
if (flow) {
LOG_MESSAGE(QString("Successfully created flow: %1").arg(flowType));
return flow;
}
} catch (...) {
LOG_MESSAGE(QString("Exception while creating flow of type: %1").arg(flowType));
}
} else {
LOG_MESSAGE(QString("No creator found for flow type: %1").arg(flowType));
}
return nullptr;
}
QStringList FlowRegistry::getAvailableTypes() const
{
return m_flowCreators.keys();
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,33 +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 <functional>
#include <QHash>
#include <QObject>
#include <QString>
namespace QodeAssist::TaskFlow {
class Flow;
class FlowManager;
class FlowRegistry : public QObject
{
Q_OBJECT
public:
using FlowCreator = std::function<Flow *(FlowManager *flowManager)>;
explicit FlowRegistry(QObject *parent = nullptr);
void registerFlow(const QString &flowType, FlowCreator creator);
Flow *createFlow(const QString &flowType, FlowManager *flowManager = nullptr) const;
QStringList getAvailableTypes() const;
private:
QHash<QString, FlowCreator> m_flowCreators;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,110 +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 "TaskConnection.hpp"
#include "BaseTask.hpp"
#include "TaskPort.hpp"
#include <QMetaEnum>
namespace QodeAssist::TaskFlow {
TaskConnection::TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent)
: QObject(parent)
, m_sourcePort(sourcePort)
, m_targetPort(targetPort)
{
setupConnection();
}
TaskConnection::~TaskConnection()
{
cleanupConnection();
}
BaseTask *TaskConnection::sourceTask() const
{
return m_sourcePort ? qobject_cast<BaseTask *>(m_sourcePort->parent()) : nullptr;
}
BaseTask *TaskConnection::targetTask() const
{
return m_targetPort ? qobject_cast<BaseTask *>(m_targetPort->parent()) : nullptr;
}
TaskPort *TaskConnection::sourcePort() const
{
return m_sourcePort;
}
TaskPort *TaskConnection::targetPort() const
{
return m_targetPort;
}
bool TaskConnection::isValid() const
{
return m_sourcePort && m_targetPort && m_sourcePort != m_targetPort && sourceTask()
&& targetTask() && sourceTask() != targetTask();
}
bool TaskConnection::isTypeCompatible() const
{
if (!isValid()) {
return false;
}
return m_targetPort->isConnectionTypeCompatible(m_sourcePort);
}
QString TaskConnection::toString() const
{
if (!isValid()) {
return QString();
}
BaseTask *srcTask = sourceTask();
BaseTask *tgtTask = targetTask();
return QString("%1.%2->%3.%4")
.arg(srcTask->taskId())
.arg(m_sourcePort->name())
.arg(tgtTask->taskId())
.arg(m_targetPort->name());
}
bool TaskConnection::operator==(const TaskConnection &other) const
{
return m_sourcePort == other.m_sourcePort && m_targetPort == other.m_targetPort;
}
void TaskConnection::setupConnection()
{
if (!isValid()) {
qWarning() << "TaskConnection::setupConnection - Invalid connection parameters";
return;
}
if (!isTypeCompatible()) {
QMetaEnum metaEnum = QMetaEnum::fromType<TaskPort::ValueType>();
qWarning() << "TaskConnection::setupConnection - Type incompatible connection:"
<< metaEnum.valueToKey(static_cast<int>(m_sourcePort->valueType())) << "to"
<< metaEnum.valueToKey(static_cast<int>(m_targetPort->valueType()));
}
m_sourcePort->setConnection(this);
m_targetPort->setConnection(this);
}
void TaskConnection::cleanupConnection()
{
if (m_sourcePort && m_sourcePort->connection() == this) {
m_sourcePort->setConnection(nullptr);
}
if (m_targetPort && m_targetPort->connection() == this) {
m_targetPort->setConnection(nullptr);
}
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,50 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QString>
namespace QodeAssist::TaskFlow {
class BaseTask;
class TaskPort;
class TaskConnection : public QObject
{
Q_OBJECT
public:
// Constructor automatically sets up the connection
explicit TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent = nullptr);
// Destructor automatically cleans up the connection
~TaskConnection() override;
// Getters
BaseTask *sourceTask() const;
BaseTask *targetTask() const;
TaskPort *sourcePort() const;
TaskPort *targetPort() const;
// Validation
bool isValid() const;
bool isTypeCompatible() const;
// Utility
QString toString() const;
// Comparison
bool operator==(const TaskConnection &other) const;
private:
TaskPort *m_sourcePort;
TaskPort *m_targetPort;
void setupConnection();
void cleanupConnection();
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,107 +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 "TaskPort.hpp"
#include "TaskConnection.hpp"
#include <QMetaEnum>
namespace QodeAssist::TaskFlow {
TaskPort::TaskPort(const QString &name, ValueType type, QObject *parent)
: QObject(parent)
, m_name(name)
, m_valueType(type)
{}
QString TaskPort::name() const
{
return m_name;
}
void TaskPort::setValueType(ValueType type)
{
if (m_valueType != type)
m_valueType = type;
}
TaskPort::ValueType TaskPort::valueType() const
{
return m_valueType;
}
void TaskPort::setValue(const QVariant &value)
{
if (!isValueTypeCompatible(value)) {
qWarning() << "TaskPort::setValue - Type mismatch for port" << m_name << "Expected:"
<< QMetaEnum::fromType<ValueType>().valueToKey(static_cast<int>(m_valueType))
<< "Got:" << value.typeName();
}
if (m_value != value) {
m_value = value;
emit valueChanged();
}
}
QVariant TaskPort::value() const
{
if (hasConnection() && m_connection->sourcePort()) {
return m_connection->sourcePort()->m_value;
}
return m_value;
}
void TaskPort::setConnection(TaskConnection *connection)
{
if (m_connection != connection) {
m_connection = connection;
emit connectionChanged();
}
}
TaskConnection *TaskPort::connection() const
{
return m_connection;
}
bool TaskPort::hasConnection() const
{
return m_connection != nullptr;
}
bool TaskPort::isValueTypeCompatible(const QVariant &value) const
{
if (m_valueType == ValueType::Any) {
return true;
}
switch (m_valueType) {
case ValueType::String:
return value.canConvert<QString>();
case ValueType::Number:
return value.canConvert<double>() || value.canConvert<int>();
case ValueType::Boolean:
return value.canConvert<bool>();
default:
return false;
}
}
bool TaskPort::isConnectionTypeCompatible(const TaskPort *sourcePort) const
{
if (!sourcePort) {
return false;
}
if (sourcePort->valueType() == ValueType::Any || m_valueType == ValueType::Any) {
return true;
}
return sourcePort->valueType() == m_valueType;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,60 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QString>
#include <QVariant>
#include "TaskConnection.hpp"
namespace QodeAssist::TaskFlow {
class TaskPort : public QObject
{
Q_OBJECT
public:
enum class ValueType {
Any, // QVariant
String, // QString
Number, // int/double
Boolean // bool
};
Q_ENUM(ValueType)
explicit TaskPort(
const QString &name, ValueType type = ValueType::Any, QObject *parent = nullptr);
QString name() const;
ValueType valueType() const;
void setValueType(ValueType type);
void setValue(const QVariant &value);
QVariant value() const;
void setConnection(TaskConnection *connection);
TaskConnection *connection() const;
bool hasConnection() const;
bool isValueTypeCompatible(const QVariant &value) const;
bool isConnectionTypeCompatible(const TaskPort *sourcePort) const;
signals:
void valueChanged();
void connectionChanged();
private:
QString m_name;
ValueType m_valueType;
QVariant m_value;
TaskConnection *m_connection = nullptr;
};
} // namespace QodeAssist::TaskFlow
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort *)
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort::ValueType)

View File

@@ -1,44 +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 "TaskRegistry.hpp"
#include <Logger.hpp>
#include "BaseTask.hpp"
namespace QodeAssist::TaskFlow {
TaskRegistry::TaskRegistry(QObject *parent)
: QObject(parent)
{}
BaseTask *TaskRegistry::createTask(const QString &taskType, QObject *parent) const
{
LOG_MESSAGE(QString("Trying to create task: %1").arg(taskType));
if (m_creators.contains(taskType)) {
LOG_MESSAGE(QString("Found creator for task type: %1").arg(taskType));
try {
BaseTask *task = m_creators[taskType](parent);
if (task) {
LOG_MESSAGE(QString("Successfully created task: %1").arg(taskType));
return task;
}
} catch (...) {
LOG_MESSAGE(QString("Exception while creating task of type: %1").arg(taskType));
}
} else {
LOG_MESSAGE(QString("No creator found for task type: %1").arg(taskType));
}
return nullptr;
}
QStringList TaskRegistry::getAvailableTypes() const
{
return m_creators.keys();
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,36 +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 <functional>
#include <QHash>
#include <QObject>
#include <QString>
namespace QodeAssist::TaskFlow {
class BaseTask;
class TaskRegistry : public QObject
{
Q_OBJECT
public:
using TaskCreator = std::function<BaseTask *(QObject *parent)>;
explicit TaskRegistry(QObject *parent = nullptr);
template<typename T>
inline void registerTask(const QString &taskType)
{
m_creators[taskType] = [](QObject *parent) -> BaseTask * { return new T(parent); };
}
BaseTask *createTask(const QString &taskType, QObject *parent = nullptr) const;
QStringList getAvailableTypes() const;
private:
QHash<QString, TaskCreator> m_creators;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -13,6 +13,8 @@ Button {
focusPolicy: Qt.NoFocus focusPolicy: Qt.NoFocus
padding: 4 padding: 4
opacity: control.enabled ? 1.0 : 0.4
icon.width: 16 icon.width: 16
icon.height: 16 icon.height: 16

View File

@@ -49,12 +49,6 @@ void UpdateStatusWidget::showUpdateAvailable(const QString &version)
m_updateButton->setToolTip(tr("Check update information")); m_updateButton->setToolTip(tr("Check update information"));
} }
void UpdateStatusWidget::hideUpdateInfo()
{
m_versionLabel->setVisible(false);
m_updateButton->setVisible(false);
}
void UpdateStatusWidget::setChatButtonAction(QAction *action) void UpdateStatusWidget::setChatButtonAction(QAction *action)
{ {
m_chatButton->setDefaultAction(action); m_chatButton->setDefaultAction(action);

View File

@@ -24,7 +24,6 @@ public:
void setDefaultAction(QAction *action); void setDefaultAction(QAction *action);
void showUpdateAvailable(const QString &version); void showUpdateAvailable(const QString &version);
void hideUpdateInfo();
void setChatButtonAction(QAction *action); void setChatButtonAction(QAction *action);
void setChatButtonMenu(QMenu *menu); void setChatButtonMenu(QMenu *menu);

View File

@@ -1,7 +1,10 @@
add_library(Context STATIC add_library(Context STATIC
DocumentContextReader.hpp DocumentContextReader.cpp DocumentContextReader.hpp DocumentContextReader.cpp
EnvBlockFormatter.hpp EnvBlockFormatter.cpp
ChangesManager.h ChangesManager.cpp ChangesManager.h ChangesManager.cpp
ContextManager.hpp ContextManager.cpp ContextManager.hpp ContextManager.cpp
IProjectScanner.hpp
ProjectScannerQtCreator.hpp ProjectScannerQtCreator.cpp
ContentFile.hpp ContentFile.hpp
DocumentReaderQtCreator.hpp DocumentReaderQtCreator.hpp
IDocumentReader.hpp IDocumentReader.hpp
@@ -21,7 +24,7 @@ target_link_libraries(Context
QtCreator::Utils QtCreator::Utils
QtCreator::ProjectExplorer QtCreator::ProjectExplorer
PRIVATE PRIVATE
PluginLLMCore Common
QodeAssistSettings QodeAssistSettings
) )

View File

@@ -56,16 +56,16 @@ void ChangesManager::addChange(
} }
} }
QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const QString ChangesManager::getRecentChangesContext(const QString &currentFilePath) const
{ {
QString context; QString context;
for (auto it = m_documentChanges.constBegin(); it != m_documentChanges.constEnd(); ++it) { for (auto it = m_documentChanges.constBegin(); it != m_documentChanges.constEnd(); ++it) {
if (it.key() != currentDocument) { if (it.key() && it.key()->filePath().toFSPathString() == currentFilePath)
continue;
for (const auto &change : it.value()) { for (const auto &change : it.value()) {
context += change.lineContent + "\n"; context += change.lineContent + "\n";
} }
} }
}
return context; return context;
} }
@@ -282,175 +282,6 @@ ChangesManager::FileEdit ChangesManager::getFileEdit(const QString &editId) cons
return m_fileEdits.value(editId); return m_fileEdits.value(editId);
} }
QList<ChangesManager::FileEdit> ChangesManager::getPendingEdits() const
{
QMutexLocker locker(&m_mutex);
QList<FileEdit> pendingEdits;
for (const auto &edit : m_fileEdits.values()) {
if (edit.status == Pending) {
pendingEdits.append(edit);
}
}
return pendingEdits;
}
bool ChangesManager::performFileEdit(
const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg)
{
auto setError = [errorMsg](const QString &msg) {
if (errorMsg) *errorMsg = msg;
};
auto editors = Core::EditorManager::visibleEditors();
for (auto *editor : editors) {
if (!editor || !editor->document()) {
continue;
}
QString editorPath = editor->document()->filePath().toFSPathString();
if (editorPath == filePath) {
QByteArray contentBytes = editor->document()->contents();
QString currentContent = QString::fromUtf8(contentBytes);
if (oldContent.isEmpty()) {
if (auto *textEditor
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
cursor.movePosition(QTextCursor::End);
cursor.insertText(newContent);
cursor.endEditBlock();
LOG_MESSAGE(QString("Appended to open editor: %1").arg(filePath));
setError("Applied successfully (appended to end of file)");
return true;
}
}
int matchPos = currentContent.indexOf(oldContent);
if (matchPos != -1) {
if (auto *textEditor
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
cursor.setPosition(matchPos);
cursor.setPosition(matchPos + oldContent.length(), QTextCursor::KeepAnchor);
cursor.removeSelectedText();
cursor.insertText(newContent);
cursor.endEditBlock();
LOG_MESSAGE(QString("Updated open editor (exact match): %1").arg(filePath));
setError("Applied successfully (exact match)");
return true;
}
} else {
double similarity = 0.0;
QString matchedContent = findBestMatch(currentContent, oldContent, 0.82, &similarity);
if (!matchedContent.isEmpty()) {
matchPos = currentContent.indexOf(matchedContent);
if (matchPos != -1) {
if (auto *textEditor
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
cursor.setPosition(matchPos);
cursor.setPosition(matchPos + matchedContent.length(), QTextCursor::KeepAnchor);
cursor.removeSelectedText();
cursor.insertText(newContent);
cursor.endEditBlock();
LOG_MESSAGE(QString("Updated open editor (fuzzy match %1%%): %2")
.arg(qRound(similarity * 100)).arg(filePath));
setError(QString("Applied with fuzzy match (%1%% similarity)").arg(qRound(similarity * 100)));
return true;
}
}
}
LOG_MESSAGE(QString("Old content not found in open editor (best similarity: %1%%): %2")
.arg(qRound(similarity * 100)).arg(filePath));
setError(QString("Content not found. Best match: %1%% (threshold: 82%%). "
"File may have changed.").arg(qRound(similarity * 100)));
return false;
}
}
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString msg = QString("Cannot open file: %1").arg(file.errorString());
LOG_MESSAGE(QString("Failed to open file for reading: %1 - %2").arg(filePath, file.errorString()));
setError(msg);
return false;
}
QString currentContent = QString::fromUtf8(file.readAll());
file.close();
QString updatedContent;
if (oldContent.isEmpty()) {
updatedContent = currentContent + newContent;
LOG_MESSAGE(QString("Appending to file: %1").arg(filePath));
setError("Applied successfully (appended to end of file)");
}
else if (currentContent.contains(oldContent)) {
int matchPos = currentContent.indexOf(oldContent);
updatedContent = currentContent.left(matchPos)
+ newContent
+ currentContent.mid(matchPos + oldContent.length());
LOG_MESSAGE(QString("Using exact match for file update: %1 at position %2")
.arg(filePath).arg(matchPos));
setError("Applied successfully (exact match)");
} else {
double similarity = 0.0;
QString matchedContent = findBestMatch(currentContent, oldContent, 0.82, &similarity);
if (!matchedContent.isEmpty()) {
int matchPos = currentContent.indexOf(matchedContent);
if (matchPos == -1) {
QString msg = "Internal error: matched content not found in file";
LOG_MESSAGE(QString("Internal error: matched content disappeared: %1").arg(filePath));
setError(msg);
return false;
}
updatedContent = currentContent.left(matchPos)
+ newContent
+ currentContent.mid(matchPos + matchedContent.length());
LOG_MESSAGE(QString("Using fuzzy match (%1%%) for file update: %2 at position %3")
.arg(qRound(similarity * 100)).arg(filePath).arg(matchPos));
setError(QString("Applied with fuzzy match (%1%% similarity)").arg(qRound(similarity * 100)));
} else {
QString msg = QString("Content not found. Best match: %1%% (threshold: 82%%). "
"File may have changed.").arg(qRound(similarity * 100));
LOG_MESSAGE(QString("Old content not found in file (best similarity: %1%%): %2")
.arg(qRound(similarity * 100)).arg(filePath));
setError(msg);
return false;
}
}
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
QString msg = QString("Cannot write file: %1").arg(file.errorString());
LOG_MESSAGE(QString("Failed to open file for writing: %1 - %2").arg(filePath, file.errorString()));
setError(msg);
return false;
}
QTextStream out(&file);
out << updatedContent;
file.close();
LOG_MESSAGE(QString("File updated: %1").arg(filePath));
return true;
}
int ChangesManager::levenshteinDistance(const QString &s1, const QString &s2) const int ChangesManager::levenshteinDistance(const QString &s1, const QString &s2) const
{ {
const int len1 = s1.length(); const int len1 = s1.length();
@@ -1112,138 +943,6 @@ QString ChangesManager::readFileContent(const QString &filePath) const
return content; return content;
} }
bool ChangesManager::performFileEditWithDiff(
const QString &filePath,
const DiffInfo &diffInfo,
bool reverse,
QString *errorMsg)
{
LOG_MESSAGE(QString("=== performFileEditWithDiff: %1 (reverse: %2) ===")
.arg(filePath).arg(reverse ? "yes" : "no"));
auto setError = [errorMsg](const QString &msg) {
if (errorMsg) *errorMsg = msg;
};
auto editors = Core::EditorManager::visibleEditors();
LOG_MESSAGE(QString(" Checking %1 visible editor(s)").arg(editors.size()));
for (auto *editor : editors) {
if (!editor || !editor->document()) {
continue;
}
QString editorPath = editor->document()->filePath().toFSPathString();
if (editorPath == filePath) {
LOG_MESSAGE(QString(" Found open editor for: %1").arg(filePath));
QByteArray contentBytes = editor->document()->contents();
QString currentContent = QString::fromUtf8(contentBytes);
LOG_MESSAGE(QString(" Current content size: %1 bytes").arg(currentContent.size()));
QString modifiedContent = currentContent;
QString diffErrorMsg;
bool diffSuccess = applyDiffToContent(modifiedContent, diffInfo, reverse, &diffErrorMsg);
if (!diffSuccess) {
LOG_MESSAGE(QString(" Failed to apply diff: %1").arg(diffErrorMsg));
setError(diffErrorMsg);
LOG_MESSAGE(" Attempting fallback to old content-based method...");
QString oldContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
QString newContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
return performFileEdit(filePath, oldContent, newContent, errorMsg);
}
if (auto *textEditor = qobject_cast<TextEditor::TextDocument *>(editor->document())) {
QTextDocument *doc = textEditor->document();
LOG_MESSAGE(" Applying changes to text editor document...");
if (!doc) {
LOG_MESSAGE(" Document is invalid");
setError("Document pointer is null");
return false;
}
try {
QTextCursor cursor(doc);
if (cursor.isNull()) {
LOG_MESSAGE(" Cursor is invalid");
setError("Cannot create text cursor");
return false;
}
cursor.beginEditBlock();
cursor.select(QTextCursor::Document);
cursor.removeSelectedText();
cursor.insertText(modifiedContent);
cursor.endEditBlock();
LOG_MESSAGE(QString(" ✓ Successfully applied diff to open editor: %1").arg(filePath));
setError(diffErrorMsg);
return true;
} catch (...) {
LOG_MESSAGE(" Exception during document modification");
setError("Exception during document modification");
return false;
}
}
}
}
LOG_MESSAGE(" File not open in editor, modifying file directly...");
LOG_MESSAGE(" Note: Undo (Ctrl+Z) will not be available for this file until it is opened");
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString msg = QString("Cannot open file: %1").arg(file.errorString());
LOG_MESSAGE(QString(" Failed to open file for reading: %1 - %2")
.arg(filePath, file.errorString()));
setError(msg);
return false;
}
QString currentContent = QString::fromUtf8(file.readAll());
file.close();
LOG_MESSAGE(QString(" File read successfully (%1 bytes)").arg(currentContent.size()));
QString modifiedContent = currentContent;
QString diffErrorMsg;
bool diffSuccess = applyDiffToContent(modifiedContent, diffInfo, reverse, &diffErrorMsg);
if (!diffSuccess) {
LOG_MESSAGE(QString(" Failed to apply diff to file: %1").arg(diffErrorMsg));
setError(diffErrorMsg);
LOG_MESSAGE(" Attempting fallback to old content-based method...");
QString oldContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
QString newContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
return performFileEdit(filePath, oldContent, newContent, errorMsg);
}
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
QString msg = QString("Cannot write file: %1").arg(file.errorString());
LOG_MESSAGE(QString(" Failed to open file for writing: %1 - %2")
.arg(filePath, file.errorString()));
setError(msg);
return false;
}
QTextStream out(&file);
out << modifiedContent;
file.close();
LOG_MESSAGE(QString(" ✓ Successfully wrote modified content to file: %1").arg(filePath));
setError(diffErrorMsg);
return true;
}
ChangesManager::DiffInfo ChangesManager::createDiffInfo( ChangesManager::DiffInfo ChangesManager::createDiffInfo(
const QString &originalContent, const QString &originalContent,
const QString &modifiedContent, const QString &modifiedContent,
@@ -1390,263 +1089,4 @@ ChangesManager::DiffInfo ChangesManager::createDiffInfo(
return diffInfo; return diffInfo;
} }
bool ChangesManager::findHunkLocation(
const QStringList &fileLines,
const DiffHunk &hunk,
int &actualStartLine,
QString *debugInfo) const
{
LOG_MESSAGE(QString(" Searching for hunk location (expected line: %1)").arg(hunk.oldStartLine));
QString debug;
int expectedIdx = hunk.oldStartLine - 1;
if (expectedIdx >= 0 && expectedIdx < fileLines.size()) {
bool exactMatch = true;
int checkIdx = expectedIdx - hunk.contextBefore.size();
if (checkIdx < 0) {
exactMatch = false;
debug += QString(" Context before out of bounds (need %1 lines before line %2)\n")
.arg(hunk.contextBefore.size()).arg(expectedIdx + 1);
} else {
for (int i = 0; i < hunk.contextBefore.size(); ++i) {
if (fileLines[checkIdx + i] != hunk.contextBefore[i]) {
exactMatch = false;
debug += QString(" Context before mismatch at offset %1: expected '%2', got '%3'\n")
.arg(i).arg(hunk.contextBefore[i]).arg(fileLines[checkIdx + i]);
break;
}
}
}
if (exactMatch) {
for (int i = 0; i < hunk.removedLines.size(); ++i) {
int lineIdx = expectedIdx + i;
if (lineIdx >= fileLines.size() || fileLines[lineIdx] != hunk.removedLines[i]) {
exactMatch = false;
debug += QString(" Removed line mismatch at offset %1: expected '%2', got '%3'\n")
.arg(i)
.arg(hunk.removedLines[i])
.arg(lineIdx < fileLines.size() ? fileLines[lineIdx] : "<EOF>");
break;
}
}
}
if (exactMatch && !hunk.contextAfter.isEmpty()) {
int afterIdx = expectedIdx + hunk.removedLines.size();
for (int i = 0; i < hunk.contextAfter.size(); ++i) {
int lineIdx = afterIdx + i;
if (lineIdx >= fileLines.size() || fileLines[lineIdx] != hunk.contextAfter[i]) {
exactMatch = false;
debug += QString(" Context after mismatch at offset %1: expected '%2', got '%3'\n")
.arg(i)
.arg(hunk.contextAfter[i])
.arg(lineIdx < fileLines.size() ? fileLines[lineIdx] : "<EOF>");
break;
}
}
}
if (exactMatch) {
actualStartLine = expectedIdx;
LOG_MESSAGE(QString(" ✓ Found exact match at expected line %1").arg(hunk.oldStartLine));
if (debugInfo) *debugInfo = "Exact match at expected location";
return true;
} else {
debug += " Exact match at expected location failed, trying fuzzy search...\n";
}
} else {
debug += QString(" Expected location %1 is out of bounds (file has %2 lines)\n")
.arg(hunk.oldStartLine).arg(fileLines.size());
}
LOG_MESSAGE(" Trying fuzzy search within ±20 lines...");
int searchStart = qMax(0, expectedIdx - 20);
int searchEnd = qMin(fileLines.size(), expectedIdx + 20);
int bestMatchLine = -1;
int bestMatchScore = 0;
for (int searchIdx = searchStart; searchIdx < searchEnd; ++searchIdx) {
int matchScore = 0;
int totalChecks = 0;
int checkIdx = searchIdx - hunk.contextBefore.size();
if (checkIdx >= 0) {
for (int i = 0; i < hunk.contextBefore.size(); ++i) {
totalChecks++;
if (fileLines[checkIdx + i] == hunk.contextBefore[i]) {
matchScore++;
}
}
}
for (int i = 0; i < hunk.removedLines.size(); ++i) {
int lineIdx = searchIdx + i;
if (lineIdx < fileLines.size()) {
totalChecks++;
if (fileLines[lineIdx] == hunk.removedLines[i]) {
matchScore++;
}
}
}
int afterIdx = searchIdx + hunk.removedLines.size();
for (int i = 0; i < hunk.contextAfter.size(); ++i) {
int lineIdx = afterIdx + i;
if (lineIdx < fileLines.size()) {
totalChecks++;
if (fileLines[lineIdx] == hunk.contextAfter[i]) {
matchScore++;
}
}
}
if (matchScore > bestMatchScore) {
bestMatchScore = matchScore;
bestMatchLine = searchIdx;
}
}
int totalPossibleScore = hunk.contextBefore.size() + hunk.removedLines.size() + hunk.contextAfter.size();
double matchPercentage = totalPossibleScore > 0 ? (double)bestMatchScore / totalPossibleScore * 100.0 : 0.0;
if (bestMatchLine != -1 && matchPercentage >= 70.0) {
actualStartLine = bestMatchLine;
debug += QString(" ✓ Found fuzzy match at line %1 (score: %2/%3 = %4%%)\n")
.arg(bestMatchLine + 1)
.arg(bestMatchScore)
.arg(totalPossibleScore)
.arg(matchPercentage, 0, 'f', 1);
LOG_MESSAGE(QString(" ✓ Found fuzzy match at line %1 (%2%% confidence)")
.arg(bestMatchLine + 1).arg(matchPercentage, 0, 'f', 1));
if (debugInfo) *debugInfo = debug;
return true;
}
debug += QString(" ✗ No suitable match found (best: %1%% at line %2)\n")
.arg(matchPercentage, 0, 'f', 1)
.arg(bestMatchLine != -1 ? bestMatchLine + 1 : -1);
LOG_MESSAGE(QString(" ✗ Hunk location not found (best match: %1%%)").arg(matchPercentage, 0, 'f', 1));
if (debugInfo) *debugInfo = debug;
return false;
}
bool ChangesManager::applyDiffToContent(
QString &content,
const DiffInfo &diffInfo,
bool reverse,
QString *errorMsg)
{
LOG_MESSAGE(QString("=== Applying %1 to content ===").arg(reverse ? "REVERSE diff" : "diff"));
auto setError = [errorMsg](const QString &msg) {
if (errorMsg) *errorMsg = msg;
};
if (diffInfo.useFallback) {
LOG_MESSAGE(" Using fallback mode (direct content replacement)");
QString searchContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
QString replaceContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
int matchPos = content.indexOf(searchContent);
if (matchPos != -1) {
content = content.left(matchPos)
+ replaceContent
+ content.mid(matchPos + searchContent.length());
setError("Applied using fallback mode (direct replacement)");
LOG_MESSAGE(QString(" ✓ Fallback: Direct replacement successful at position %1").arg(matchPos));
return true;
} else {
setError("Fallback failed: Original content not found in file");
LOG_MESSAGE(" ✗ Fallback: Content not found");
return false;
}
}
if (diffInfo.hunks.isEmpty()) {
LOG_MESSAGE(" No hunks to apply (content unchanged)");
setError("No changes to apply");
return true;
}
QStringList fileLines = content.split('\n');
LOG_MESSAGE(QString(" File has %1 lines, applying %2 hunk(s)")
.arg(fileLines.size()).arg(diffInfo.hunks.size()));
QList<DiffHunk> hunksToApply = diffInfo.hunks;
std::sort(hunksToApply.begin(), hunksToApply.end(),
[](const DiffHunk &a, const DiffHunk &b) {
return a.oldStartLine > b.oldStartLine;
});
LOG_MESSAGE(" Hunks sorted in descending order for application");
int appliedHunks = 0;
int failedHunks = 0;
for (int hunkIdx = 0; hunkIdx < hunksToApply.size(); ++hunkIdx) {
const DiffHunk &hunk = hunksToApply[hunkIdx];
LOG_MESSAGE(QString(" --- Applying hunk %1/%2 ---")
.arg(hunkIdx + 1).arg(hunksToApply.size()));
int actualStartLine = -1;
QString debugInfo;
if (!findHunkLocation(fileLines, hunk, actualStartLine, &debugInfo)) {
LOG_MESSAGE(QString(" ✗ Failed to locate hunk %1:\n%2")
.arg(hunkIdx + 1).arg(debugInfo));
failedHunks++;
continue;
}
LOG_MESSAGE(QString(" Applying hunk at line %1 (remove %2 lines, add %3 lines)")
.arg(actualStartLine + 1)
.arg(hunk.removedLines.size())
.arg(hunk.addedLines.size()));
for (int i = 0; i < hunk.removedLines.size(); ++i) {
if (actualStartLine < fileLines.size()) {
LOG_MESSAGE(QString(" Removing line %1: '%2'")
.arg(actualStartLine + 1)
.arg(fileLines[actualStartLine]));
fileLines.removeAt(actualStartLine);
}
}
for (int i = 0; i < hunk.addedLines.size(); ++i) {
LOG_MESSAGE(QString(" Inserting line %1: '%2'")
.arg(actualStartLine + i + 1)
.arg(hunk.addedLines[i]));
fileLines.insert(actualStartLine + i, hunk.addedLines[i]);
}
appliedHunks++;
LOG_MESSAGE(QString(" ✓ Hunk %1 applied successfully").arg(hunkIdx + 1));
}
if (failedHunks > 0) {
QString msg = QString("Partially applied: %1 of %2 hunks succeeded")
.arg(appliedHunks).arg(hunksToApply.size());
setError(msg);
LOG_MESSAGE(QString(" ⚠ %1").arg(msg));
content = fileLines.join('\n');
return false;
}
content = fileLines.join('\n');
setError(QString("Successfully applied %1 hunk(s)").arg(appliedHunks));
LOG_MESSAGE(QString("=== All %1 hunk(s) applied successfully ===").arg(appliedHunks));
return true;
}
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -26,7 +26,7 @@ public:
QString lineContent; QString lineContent;
}; };
enum FileEditStatus { Pending, Applied, Rejected, Archived }; enum FileEditStatus : int { Pending, Applied, Rejected, Archived };
struct DiffHunk struct DiffHunk
{ {
@@ -67,7 +67,7 @@ public:
void addChange( void addChange(
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded); TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const; QString getRecentChangesContext(const QString &currentFilePath) const;
void addFileEdit( void addFileEdit(
const QString &editId, const QString &editId,
@@ -81,7 +81,6 @@ public:
bool rejectFileEdit(const QString &editId); bool rejectFileEdit(const QString &editId);
bool undoFileEdit(const QString &editId); bool undoFileEdit(const QString &editId);
FileEdit getFileEdit(const QString &editId) const; FileEdit getFileEdit(const QString &editId) const;
QList<FileEdit> getPendingEdits() const;
bool applyPendingEditsForRequest(const QString &requestId, QString *errorMsg = nullptr); bool applyPendingEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
@@ -106,13 +105,10 @@ private:
ChangesManager(const ChangesManager &) = delete; ChangesManager(const ChangesManager &) = delete;
ChangesManager &operator=(const ChangesManager &) = delete; ChangesManager &operator=(const ChangesManager &) = delete;
bool performFileEdit(const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg = nullptr);
bool performFileEditWithDiff(const QString &filePath, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
QString readFileContent(const QString &filePath) const; QString readFileContent(const QString &filePath) const;
DiffInfo createDiffInfo(const QString &originalContent, const QString &modifiedContent, const QString &filePath); DiffInfo createDiffInfo(
bool applyDiffToContent(QString &content, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr); const QString &originalContent, const QString &modifiedContent, const QString &filePath);
bool findHunkLocation(const QStringList &fileLines, const DiffHunk &hunk, int &actualStartLine, QString *debugInfo = nullptr) const;
// Helper method for fragment-based apply/undo operations // Helper method for fragment-based apply/undo operations
bool performFragmentReplacement( bool performFragmentReplacement(

View File

@@ -6,25 +6,24 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonObject>
#include <QTextStream> #include <QTextStream>
#include "settings/GeneralSettings.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h>
#include <texteditor/textdocument.h>
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectScannerQtCreator.hpp"
namespace QodeAssist::Context { namespace QodeAssist::Context {
ContextManager::ContextManager(QObject *parent) ContextManager::ContextManager(QObject *parent)
: QObject(parent) : ContextManager(std::make_unique<ProjectScannerQtCreator>(), parent)
, m_ignoreManager(new IgnoreManager(this))
{} {}
ContextManager::ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent)
: QObject(parent)
, m_scanner(std::move(scanner))
{}
ContextManager::~ContextManager() = default;
QString ContextManager::readFile(const QString &filePath) const QString ContextManager::readFile(const QString &filePath) const
{ {
QFile file(filePath); QFile file(filePath);
@@ -45,9 +44,7 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
{ {
QList<ContentFile> files; QList<ContentFile> files;
for (const QString &path : filePaths) { for (const QString &path : filePaths) {
auto project = ProjectExplorer::ProjectManager::projectForFile( if (m_scanner->shouldIgnore(path)) {
Utils::FilePath::fromString(path));
if (project && m_ignoreManager->shouldIgnore(path, project)) {
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path)); LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
continue; continue;
} }
@@ -58,27 +55,6 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
return files; return files;
} }
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
{
QStringList sourceFiles;
if (!project)
return sourceFiles;
auto projectNode = project->rootProjectNode();
if (!projectNode)
return sourceFiles;
projectNode->forEachNode(
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
sourceFiles.append(fileNode->filePath().toUrlishString());
}
},
nullptr);
return sourceFiles;
}
ContentFile ContextManager::createContentFile(const QString &filePath) const ContentFile ContextManager::createContentFile(const QString &filePath) const
{ {
ContentFile contentFile; ContentFile contentFile;
@@ -100,77 +76,26 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
{ {
const auto &generalSettings = Settings::generalSettings(); Q_UNUSED(documentInfo)
return false;
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
} }
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const QString ContextManager::openedFilesContext(const QStringList &excludeFiles) const
{
auto documents = Core::DocumentModel::openedDocuments();
QList<QPair<QString, QString>> files;
for (const auto *document : std::as_const(documents)) {
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
if (!excludeFiles.contains(filePath)) {
files.append({filePath, textDocument->plainText()});
}
}
return files;
}
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
{ {
QString context = "User files context:\n"; QString context = "User files context:\n";
auto documents = Core::DocumentModel::openedDocuments(); for (const auto &file : m_scanner->openedTextFiles(excludeFiles)) {
context += QString("File: %1\n").arg(file.filePath);
for (const auto *document : documents) { context += file.content;
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
if (!textDocument)
continue;
auto filePath = textDocument->filePath().toUrlishString();
if (excludeFiles.contains(filePath))
continue;
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
continue;
}
context += QString("File: %1\n").arg(filePath);
context += textDocument->plainText();
context += "\n"; context += "\n";
} }
return context; return context;
} }
IgnoreManager *ContextManager::ignoreManager() const bool ContextManager::shouldIgnore(const QString &filePath) const
{ {
return m_ignoreManager; return m_scanner->shouldIgnore(filePath);
} }
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -4,18 +4,16 @@
#pragma once #pragma once
#include <memory>
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include "ContentFile.hpp" #include "ContentFile.hpp"
#include "IContextManager.hpp" #include "IContextManager.hpp"
#include "IgnoreManager.hpp" #include "IProjectScanner.hpp"
#include "ProgrammingLanguage.hpp" #include "ProgrammingLanguage.hpp"
namespace ProjectExplorer {
class Project;
}
namespace QodeAssist::Context { namespace QodeAssist::Context {
class ContextManager : public QObject, public IContextManager class ContextManager : public QObject, public IContextManager
@@ -24,22 +22,22 @@ class ContextManager : public QObject, public IContextManager
public: public:
explicit ContextManager(QObject *parent = nullptr); explicit ContextManager(QObject *parent = nullptr);
~ContextManager() override = default; ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent = nullptr);
~ContextManager() override;
QString readFile(const QString &filePath) const override; QString readFile(const QString &filePath) const override;
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override; QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
ContentFile createContentFile(const QString &filePath) const override; ContentFile createContentFile(const QString &filePath) const override;
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override; ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override; bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
IgnoreManager *ignoreManager() const; QString openedFilesContext(const QStringList &excludeFiles = QStringList{}) const;
bool shouldIgnore(const QString &filePath) const;
private: private:
IgnoreManager *m_ignoreManager; std::unique_ptr<IProjectScanner> m_scanner;
}; };
} // namespace QodeAssist::Context } // namespace QodeAssist::Context

View File

@@ -4,13 +4,12 @@
#include "DocumentContextReader.hpp" #include "DocumentContextReader.hpp"
#include <languageserverprotocol/lsptypes.h>
#include <QFileInfo>
#include <QTextBlock> #include <QTextBlock>
#include "CodeCompletionSettings.hpp" #include "CodeCompletionSettings.hpp"
#include "ChangesManager.h" #include "ChangesManager.h"
#include "EnvBlockFormatter.hpp"
const QRegularExpression &getYearRegex() const QRegularExpression &getYearRegex()
{ {
@@ -108,15 +107,6 @@ QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosi
return getContextBetween(lineNumber, cursorPosition, endLine, -1); return getContextBetween(lineNumber, cursorPosition, endLine, -1);
} }
QString DocumentContextReader::getLanguageAndFileInfo() const
{
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_mimeType);
QString fileExtension = QFileInfo(m_filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, m_mimeType, m_filePath, fileExtension);
}
CopyrightInfo DocumentContextReader::findCopyright() CopyrightInfo DocumentContextReader::findCopyright()
{ {
CopyrightInfo result = {-1, -1, false}; CopyrightInfo result = {-1, -1, false};
@@ -249,12 +239,7 @@ QString DocumentContextReader::getContextBetween(
return context; return context;
} }
CopyrightInfo DocumentContextReader::copyrightInfo() const Templates::ContextData DocumentContextReader::prepareContext(
{
return m_copyrightInfo;
}
PluginLLMCore::ContextData DocumentContextReader::prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
{ {
QString contextBefore; QString contextBefore;
@@ -272,11 +257,13 @@ PluginLLMCore::ContextData DocumentContextReader::prepareContext(
} }
QString fileContext; QString fileContext;
fileContext.append("\n ").append(getLanguageAndFileInfo()); fileContext.append("\n")
.append(EnvBlockFormatter::formatFile({m_filePath, m_mimeType}))
.append("\n");
if (settings.useProjectChangesCache()) if (settings.useProjectChangesCache())
fileContext.append("Recent Project Changes Context:\n ") fileContext.append("Recent Project Changes Context:\n ")
.append(ChangesManager::instance().getRecentChangesContext(m_textDocument)); .append(ChangesManager::instance().getRecentChangesContext(m_filePath));
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext}; return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext};
} }

View File

@@ -4,11 +4,10 @@
#pragma once #pragma once
#include <texteditor/textdocument.h>
#include <QTextDocument> #include <QTextDocument>
#include <pluginllmcore/ContextData.hpp>
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
#include <sources/common/ContextData.hpp>
namespace QodeAssist::Context { namespace QodeAssist::Context {
@@ -51,18 +50,14 @@ public:
*/ */
QString readWholeFileAfter(int lineNumber, int cursorPosition) const; QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
QString getLanguageAndFileInfo() const;
CopyrightInfo findCopyright(); CopyrightInfo findCopyright();
QString getContextBetween( QString getContextBetween(
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const; int startLine, int startCursorPosition, int endLine, int endCursorPosition) const;
CopyrightInfo copyrightInfo() const; Templates::ContextData prepareContext(
PluginLLMCore::ContextData prepareContext(
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const; int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
private: private:
TextEditor::TextDocument *m_textDocument;
QTextDocument *m_document; QTextDocument *m_document;
QString m_mimeType; QString m_mimeType;
QString m_filePath; QString m_filePath;

Some files were not shown because too many files have changed in this diff Show More