Compare commits

..

19 Commits

Author SHA1 Message Date
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
Petr Mironychev
34ce787320 Update repository URL description in README 2026-06-17 15:16:52 +02:00
Petr Mironychev
52ef8318dc chore: Update plugin to 0.9.21 version 2026-06-17 14:16:50 +02:00
Petr Mironychev
449317ab91 feat: Add support QtCreator 20 (#352) 2026-06-17 14:15:20 +02:00
Petr Mironychev
abb3351246 chore: Update version to 0.9.20 2026-06-08 15:51:03 +02:00
Petr Mironychev
57eeb32ceb chore: Update translations 2026-06-08 15:36:09 +02:00
Petr Mironychev
74eed49fb4 feat: Add transfer timeout settings 2026-06-08 15:20:21 +02:00
Petr Mironychev
43a30281b6 feat: Improve BusyIndicator 2026-06-08 12:53:37 +02:00
Petr Mironychev
bf4307c459 doc: Update License in README 2026-06-08 12:31:14 +02:00
Petr Mironychev
6df70e608b chore: Update LICENCE and copyright 2026-06-08 11:25:18 +02:00
Petr Mironychev
ee1bf4ffe5 feat: Improve chat, status and message sending keys (#361) 2026-06-06 11:25:30 +02:00
569 changed files with 13323 additions and 19063 deletions

View File

@@ -27,7 +27,7 @@ jobs:
config:
- {
name: "Windows Latest MSVC", artifact: "Windows-x64",
os: windows-latest,
os: windows-2022,
platform: windows_x64,
cc: "cl", cxx: "cl",
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
@@ -53,6 +53,10 @@ jobs:
qt_version: "6.10.3",
qt_creator_version: "19.0.2"
}
- {
qt_version: "6.11.1",
qt_creator_version: "20.0.0"
}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
@@ -110,10 +114,14 @@ jobs:
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
string(REPLACE "." "" qt_version_dotless "${qt_version}")
set(qt_repo_dir "qt6_${qt_version_dotless}")
if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_msvc2022_64")
set(qt_dir_prefix "${qt_version}/msvc2022_64")
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
set(qt_repo_dir "qt6_${qt_version_dotless}_msvc2022_64")
endif()
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
else()
@@ -127,7 +135,9 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64")
endif()
set(qt_dir_prefix "${qt_version}/gcc_64")
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
set(qt_package_suffix "-Linux-RHEL_9_6-GCC-Linux-RHEL_9_6-X86_64")
elseif (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
else()
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
@@ -143,7 +153,7 @@ jobs:
endif()
endif()
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/${qt_repo_dir}")
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
file(READ ./Updates.xml updates_xml)
@@ -170,7 +180,11 @@ jobs:
)
endforeach()
foreach(package qt5compat qtshadertools)
set(qt_addon_packages qt5compat qtshadertools)
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
list(APPEND qt_addon_packages qttasktree)
endif()
foreach(package ${qt_addon_packages})
downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z
@@ -236,7 +250,7 @@ jobs:
endif()
set(build_plugin_py "scripts/build_plugin.py")
foreach(dir "share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts")
foreach(dir "share/qtcreator/scripts" "Qt Creator.sdk/share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts")
if(EXISTS "${{ steps.qt_creator.outputs.qtc_dir }}/${dir}/build_plugin.py")
set(build_plugin_py "${dir}/build_plugin.py")
break()

View File

@@ -2,10 +2,6 @@ cmake_minimum_required(VERSION 3.16)
project(QodeAssist)
option(QODEASSIST_EXPERIMENTAL
"Enable experimental features" OFF)
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
@@ -42,7 +38,6 @@ add_definitions(
add_subdirectory(sources)
add_subdirectory(logger)
add_subdirectory(pluginllmcore)
add_subdirectory(settings)
add_subdirectory(UIControls)
add_subdirectory(ChatView)
@@ -69,7 +64,6 @@ add_qtc_plugin(QodeAssist
QtCreator::Utils
QtCreator::CPlusPlus
LLMQore
PluginLLMCore
ProvidersConfig
Agents
Skills
@@ -83,42 +77,6 @@ add_qtc_plugin(QodeAssist
QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp
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
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
@@ -130,17 +88,12 @@ add_qtc_plugin(QodeAssist
chat/ChatDocument.hpp chat/ChatDocument.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
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/ErrorWidget.hpp widgets/ErrorWidget.cpp
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
@@ -170,10 +123,7 @@ add_qtc_plugin(QodeAssist
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
)
if(QODEASSIST_EXPERIMENTAL)
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
endif()
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable

View File

@@ -1,123 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,38 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml
qml/controls/ContextViewer.qml
qml/controls/SkillCommandPopup.qml
qml/controls/Toast.qml
qml/controls/TopBar.qml
@@ -45,9 +44,8 @@ qt_add_qml_module(QodeAssistChatView
icons/window-unlock.svg
icons/chat-icon.svg
icons/chat-pause-icon.svg
icons/warning-icon.svg
icons/new-chat-icon.svg
icons/rules-icon.svg
icons/context-icon.svg
icons/open-in-editor.svg
icons/open-in-window.svg
icons/apply-changes-button.svg
@@ -74,8 +72,7 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp
AgentRoleController.hpp AgentRoleController.cpp
ChatConfigurationController.hpp ChatConfigurationController.cpp
ChatAgentController.hpp ChatAgentController.cpp
FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp
@@ -91,13 +88,14 @@ target_link_libraries(QodeAssistChatView
Qt::Network
QtCreator::Core
QtCreator::Utils
PluginLLMCore
QodeAssistSettings
Context
QodeAssistUIControlsplugin
QodeAssistLogger
LLMQore
Skills
Agents
Session
)
target_include_directories(QodeAssistChatView

View File

@@ -0,0 +1,108 @@
// 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();
}
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

@@ -1,21 +1,34 @@
// 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 "ChatCompressor.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp>
#include "ChatModel.hpp"
#include <LLMQore/ContentBlocks.hpp>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.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 <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStringList>
#include <QUuid>
namespace QodeAssist::Chat {
@@ -24,7 +37,18 @@ ChatCompressor::ChatCompressor(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) {
emit compressionFailed(tr("Compression already in progress"));
@@ -36,47 +60,84 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
return;
}
if (!chatModel || chatModel->rowCount() == 0) {
if (!sourceHistory || sourceHistory->isEmpty()) {
emit compressionFailed(tr("Chat is empty, nothing to compress"));
return;
}
auto providerName = Settings::generalSettings().caProvider();
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
if (!m_sessionManager) {
emit compressionFailed(tr("Chat session manager is not available"));
return;
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) {
emit compressionFailed(tr("No template available"));
QString sessionError;
Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
if (!session) {
emit compressionFailed(
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
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_chatModel = chatModel;
m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear();
m_session = session;
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{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
const QString role = msg.role() == Message::Role::User ? QStringLiteral("User")
: 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 endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
m_currentRequestId = m_provider->sendRequest(
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
const QString transcript = transcriptParts.join(QStringLiteral("\n\n"));
connect(
session, &Session::finished, this,
[this](const LLMQore::RequestID &id, const QString &) { 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));
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));
}
@@ -91,44 +152,38 @@ void ChatCompressor::cancelCompression()
return;
LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState();
emit compressionFailed(tr("Compression cancelled"));
}
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
void ChatCompressor::onCompressionFinished(const QString &requestId)
{
if (!m_isCompressing || requestId != m_currentRequestId)
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)
{
Q_UNUSED(fullText)
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
if (!m_isCompressing || requestId != m_currentRequestId)
return;
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
const QString sourcePath = m_originalChatPath;
LOG_MESSAGE(
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
cleanupState();
QString compressedPath = createCompressedChatPath(m_originalChatPath);
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;
}
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
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)
return;
@@ -151,53 +206,6 @@ QString ChatCompressor::createCompressedChatPath(const QString &originalPath) co
.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(
const QString &sourcePath, const QString &destPath, const QString &summary)
{
@@ -221,11 +229,11 @@ bool ChatCompressor::createCompressedChatFile(
QJsonObject summaryMessage;
summaryMessage["role"] = "assistant";
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
summaryMessage["isRedacted"] = false;
summaryMessage["attachments"] = QJsonArray();
summaryMessage["images"] = QJsonArray();
QJsonObject textBlock;
textBlock["type"] = "text";
textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["blocks"] = QJsonArray{textBlock};
root["messages"] = QJsonArray{summaryMessage};
root["compressedFrom"] = sourcePath;
@@ -244,49 +252,17 @@ bool ChatCompressor::createCompressedChatFile(
return true;
}
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));
}
void ChatCompressor::disconnectAllSignals()
{
for (const auto &connection : std::as_const(m_connections))
disconnect(connection);
m_connections.clear();
}
void ChatCompressor::cleanupState()
{
disconnectAllSignals();
Session *session = m_session;
m_isCompressing = false;
m_currentRequestId.clear();
m_originalChatPath.clear();
m_accumulatedSummary.clear();
m_chatModel = nullptr;
m_provider = nullptr;
m_session = nullptr;
if (session && m_sessionManager)
m_sessionManager->release(session);
}
} // namespace QodeAssist::Chat

View File

@@ -1,22 +1,22 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QJsonObject>
#include <QList>
#include <QObject>
#include <QPointer>
#include <QString>
namespace QodeAssist::PluginLLMCore {
class Provider;
class PromptTemplate;
} // namespace QodeAssist::PluginLLMCore
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject
{
Q_OBJECT
@@ -24,7 +24,10 @@ class ChatCompressor : public QObject
public:
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;
void cancelCompression();
@@ -34,30 +37,22 @@ signals:
void compressionCompleted(const QString &compressedChatPath);
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:
void onCompressionFinished(const QString &requestId);
void onCompressionFailed(const QString &requestId, const QString &error);
QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const;
bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState();
void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false;
QString m_currentRequestId;
QString m_originalChatPath;
QString m_accumulatedSummary;
PluginLLMCore::Provider *m_provider = nullptr;
ChatModel *m_chatModel = nullptr;
QList<QMetaObject::Connection> m_connections;
QPointer<SessionManager> m_sessionManager;
QString m_activeAgent;
QPointer<Session> m_session;
};
} // namespace QodeAssist::Chat

View File

@@ -1,99 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,35 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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 "ChatFileManager.hpp"
#include "Logger.hpp"

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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 "ChatHistoryStore.hpp"
@@ -15,15 +16,20 @@
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include "ChatModel.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "Logger.hpp"
#include "ProjectSettings.hpp"
namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
: QObject(parent)
, m_chatModel(chatModel)
, m_history(history)
{}
QString ChatHistoryStore::historyDir() const
@@ -51,17 +57,23 @@ QString ChatHistoryStore::suggestedFileName() const
{
QString shortMessage;
if (m_chatModel->rowCount() > 0) {
QString firstMessage
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
shortMessage = firstMessage.split('\n').first().simplified().left(30);
if (m_history) {
for (const auto &message : m_history->messages()) {
if (message.role() != Message::Role::User)
continue;
if (shortMessage.isEmpty()) {
QVariantList images
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
if (!images.isEmpty()) {
shortMessage = "image_chat";
const QString text = message.text();
if (!text.trimmed().isEmpty()) {
shortMessage = text.split('\n').first().simplified().left(30);
} else {
for (const auto &block : message.blocks()) {
if (dynamic_cast<StoredImageContent *>(block.get())) {
shortMessage = "image_chat";
break;
}
}
}
break;
}
}
@@ -106,12 +118,12 @@ QString ChatHistoryStore::autosaveFilePath(
SerializationResult ChatHistoryStore::save(const QString &filePath) const
{
return ChatSerializer::saveToFile(m_chatModel, filePath);
return ChatSerializer::saveToFile(m_history, filePath);
}
SerializationResult ChatHistoryStore::load(const QString &filePath) const
{
return ChatSerializer::loadFromFile(m_chatModel, filePath);
return ChatSerializer::loadFromFile(m_history, filePath);
}
void ChatHistoryStore::showSaveDialog()

View File

@@ -1,5 +1,6 @@
// 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
@@ -8,16 +9,18 @@
#include "ChatSerializer.hpp"
namespace QodeAssist::Chat {
namespace QodeAssist {
class ConversationHistory;
}
class ChatModel;
namespace QodeAssist::Chat {
class ChatHistoryStore : public QObject
{
Q_OBJECT
public:
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
QString historyDir() const;
QString suggestedFileName() const;
@@ -41,7 +44,7 @@ signals:
private:
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
ChatModel *m_chatModel;
ConversationHistory *m_history;
};
} // namespace QodeAssist::Chat

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
// 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
@@ -7,11 +8,16 @@
#include "MessagePart.hpp"
#include <QAbstractListModel>
#include <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QPointer>
#include <QVector>
#include <QtQmlIntegration>
#include "context/ContentFile.hpp"
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat {
@@ -21,14 +27,13 @@ class ChatModel : public QAbstractListModel
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
QML_ELEMENT
public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
enum ChatRole : int { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole)
enum Roles {
enum Roles : int {
RoleType = Qt::UserRole,
Content,
Attachments,
@@ -42,81 +47,19 @@ public:
};
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);
void setHistory(ConversationHistory *history);
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;
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 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 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(
const QString &messageId,
int promptTokens,
@@ -127,11 +70,7 @@ public:
int sessionPromptTokens() const;
int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const;
int sessionTotalTokens() const;
void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const;
void setChatFilePath(const QString &filePath);
QString chatFilePath() const;
@@ -140,18 +79,64 @@ signals:
void sessionUsageChanged();
private slots:
void onFileEditApplied(const QString &editId);
void onFileEditRejected(const QString &editId);
void onFileEditArchived(const QString &editId);
void onHistoryMessageAdded(int index);
void onHistoryMessageUpdated(int index);
void onHistoryCleared();
void onHistoryReset();
void onFileEditStatusChanged(const QString &editId);
private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
QVector<Message> m_messages;
bool m_loadingFromHistory = false;
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;
};
void rebuildAll();
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;
};
} // namespace QodeAssist::Chat
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

View File

@@ -1,5 +1,6 @@
// 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 "ChatRootView.hpp"
@@ -10,6 +11,7 @@
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QKeySequence>
#include <QMessageBox>
#include <QQmlContext>
#include <QQmlEngine>
@@ -26,9 +28,14 @@
#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 "ChatConfigurationController.hpp"
#include "ChatCompressor.hpp"
#include "ChatHistoryStore.hpp"
#include "FileEditController.hpp"
@@ -36,10 +43,8 @@
#include "InputTokenCounter.hpp"
#include "SettingsConstants.hpp"
#include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp"
#include "pluginllmcore/RulesLoader.hpp"
#include "ProjectSettings.hpp"
#include "SkillsSettings.hpp"
#include "sources/skills/SkillsManager.hpp"
@@ -52,23 +57,39 @@ bool isChatEditor(Core::IEditor *editor)
return editor && editor->document()
&& editor->document()->id() == Utils::Id(Constants::QODE_ASSIST_CHAT_EDITOR_ID);
}
QKeySequence sendMessageKeySequence()
{
auto command = Core::ActionManager::command(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE);
if (!command)
return {};
QKeySequence sequence = command->keySequence();
if (sequence.isEmpty()) {
const QList<QKeySequence> defaults = command->defaultKeySequences();
if (!defaults.isEmpty())
sequence = defaults.constFirst();
}
return sequence;
}
} // namespace
ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent)
, m_history(new QodeAssist::ConversationHistory(this))
, m_chatModel(new ChatModel(this))
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_clientInterface(new ClientInterface(m_chatModel, this))
, m_fileManager(new ChatFileManager(this))
, m_isRequestInProgress(false)
, m_chatCompressor(new ChatCompressor(this))
, m_agentRoleController(new AgentRoleController(this))
, m_configurationController(new ChatConfigurationController(this))
, m_fileEditController(new FileEditController(m_chatModel, this))
, m_tokenCounter(
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
, m_agentController(new ChatAgentController(this))
, m_fileEditController(new FileEditController(this))
, m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_history, this))
{
m_chatModel->setHistory(m_history);
m_clientInterface->setHistory(m_history);
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect(
&Settings::chatAssistantSettings().linkOpenFiles,
@@ -76,21 +97,21 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); });
auto &settings = Settings::generalSettings();
connect(
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
connect(
m_configurationController,
&ChatConfigurationController::availableConfigurationsChanged,
QMetaObject::invokeMethod(
this,
&ChatRootView::availableConfigurationsChanged);
connect(
m_configurationController,
&ChatConfigurationController::currentConfigurationChanged,
this,
&ChatRootView::currentConfigurationChanged);
[this] {
if (auto sendCommand
= Core::ActionManager::command(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE)) {
connect(
sendCommand,
&Core::Command::keySequenceChanged,
this,
&ChatRootView::sendShortcutTextChanged,
Qt::UniqueConnection);
}
emit sendShortcutTextChanged();
},
Qt::QueuedConnection);
connect(
m_clientInterface,
@@ -110,6 +131,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
setRecentFilePath(QString{});
m_tokenCounter->resetServerUsage();
m_fileEditController->clearCurrentRequestId();
});
auto maybeEmitTitle = [this] {
@@ -138,20 +160,20 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&ChatRootView::inputTokensCountChanged);
connect(
m_agentRoleController,
&AgentRoleController::availableRolesChanged,
m_agentController,
&ChatAgentController::availableAgentsChanged,
this,
&ChatRootView::availableAgentRolesChanged);
&ChatRootView::availableChatAgentsChanged);
connect(
m_agentRoleController,
&AgentRoleController::currentRoleChanged,
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::currentAgentRoleChanged);
&ChatRootView::currentChatAgentChanged);
connect(
m_agentRoleController,
&AgentRoleController::baseSystemPromptChanged,
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::baseSystemPromptChanged);
&ChatRootView::useToolsChanged);
auto editors = Core::EditorManager::instance();
@@ -210,8 +232,8 @@ ChatRootView::ChatRootView(QQuickItem *parent)
m_clientInterface,
&ClientInterface::messageUsageReceived,
this,
[this](int promptTokens, int /*completionTokens*/, int /*cached*/, int /*reasoning*/) {
m_tokenCounter->recordServerUsage(promptTokens);
[this](int promptTokens, int /*completionTokens*/, int cachedTokens, int /*reasoning*/) {
m_tokenCounter->recordServerUsage(promptTokens, cachedTokens);
});
connect(
@@ -233,14 +255,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
refreshRules();
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded,
@@ -253,24 +267,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&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) {
m_lastErrorMessage = error;
emit lastErrorMessageChanged();
@@ -291,7 +287,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) {
PendingSend p = m_pendingSend;
m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
dispatchSend(p.message, p.attachments, p.linkedFiles);
}
});
@@ -304,7 +300,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) {
PendingSend p = m_pendingSend;
m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
dispatchSend(p.message, p.attachments, p.linkedFiles);
}
});
}
@@ -340,6 +336,57 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
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 results;
@@ -347,7 +394,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
if (!manager || !Settings::skillsSettings().enableSkills())
return results;
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
auto *project = ProjectExplorer::ProjectManager::startupProject();
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
@@ -383,21 +430,17 @@ void ChatRootView::sendMessage(const QString &message)
{
const QStringList attachments = m_attachmentFiles;
const QStringList linkedFiles = m_linkedFiles;
const bool tools = useTools();
const bool thinking = useThinking();
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking))
if (deferSendForAutoCompress(message, attachments, linkedFiles))
return;
dispatchSend(message, attachments, linkedFiles, tools, thinking);
dispatchSend(message, attachments, linkedFiles);
}
bool ChatRootView::deferSendForAutoCompress(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
const QStringList &linkedFiles)
{
auto &settings = Settings::chatAssistantSettings();
if (!settings.autoCompress())
@@ -408,6 +451,9 @@ bool ChatRootView::deferSendForAutoCompress(
if (inputTokens < threshold)
return false;
if (configuredCompressionAgent().isEmpty())
return false;
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments);
if (filePath.isEmpty())
@@ -423,7 +469,7 @@ bool ChatRootView::deferSendForAutoCompress(
.arg(inputTokens)
.arg(threshold));
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
m_pendingSend = {message, attachments, linkedFiles, true};
compressCurrentChat();
return true;
}
@@ -431,9 +477,7 @@ bool ChatRootView::deferSendForAutoCompress(
void ChatRootView::dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useToolsArg,
bool useThinkingArg)
const QStringList &linkedFiles)
{
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments);
@@ -446,10 +490,13 @@ void ChatRootView::dispatchSend(
}
}
m_tokenCounter->recordSent();
if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
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();
clearAttachmentFiles();
@@ -494,21 +541,14 @@ void ChatRootView::clearMessages()
clearLinkedFiles();
}
QString ChatRootView::currentTemplate() const
{
auto &settings = Settings::generalSettings();
return settings.caModel();
}
void ChatRootView::saveHistory(const QString &filePath)
{
if (filePath != m_recentFilePath) {
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
m_lastErrorMessage
= tr("This chat file is already in use by another QodeAssist chat session.");
emit lastErrorMessageChanged();
return;
}
if (auto registry = sessionFileRegistry();
registry && registry->isLockedByOther(filePath, ownerSession())) {
m_lastErrorMessage = tr(
"This chat file is already in use by another QodeAssist chat session.");
emit lastErrorMessageChanged();
return;
}
auto result = m_historyStore->save(filePath);
@@ -521,13 +561,11 @@ void ChatRootView::saveHistory(const QString &filePath)
void ChatRootView::loadHistory(const QString &filePath)
{
if (filePath != m_recentFilePath) {
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
m_lastErrorMessage
= tr("This chat is already open in another QodeAssist chat session.");
emit lastErrorMessageChanged();
return;
}
if (auto registry = sessionFileRegistry();
registry && registry->isLockedByOther(filePath, ownerSession())) {
m_lastErrorMessage = tr("This chat is already open in another QodeAssist chat session.");
emit lastErrorMessageChanged();
return;
}
auto result = m_historyStore->load(filePath);
@@ -743,6 +781,32 @@ void ChatRootView::calculateMessageTokensCount(const QString &message)
m_tokenCounter->setMessage(message);
}
bool ChatRootView::isSendShortcut(int key, int modifiers) const
{
const QKeySequence sequence = sendMessageKeySequence();
if (sequence.isEmpty())
return false;
const QKeyCombination combination = sequence[0];
const int sequenceKey = combination.key();
const int relevantMask = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier
| Qt::MetaModifier;
const int sequenceModifiers = combination.keyboardModifiers() & relevantMask;
const int eventModifiers = modifiers & relevantMask;
const bool isReturnLike = sequenceKey == Qt::Key_Return || sequenceKey == Qt::Key_Enter;
const bool keyMatches = key == sequenceKey
|| (isReturnLike && (key == Qt::Key_Return || key == Qt::Key_Enter));
return keyMatches && eventModifiers == sequenceModifiers;
}
QString ChatRootView::sendShortcutText() const
{
return sendMessageKeySequence().toString(QKeySequence::NativeText);
}
void ChatRootView::setIsSyncOpenFiles(bool state)
{
if (m_isSyncOpenFiles != state) {
@@ -762,25 +826,6 @@ void ChatRootView::openChatHistoryFolder()
m_historyStore->openHistoryFolder();
}
void ChatRootView::openRulesFolder()
{
auto project = ProjectExplorer::ProjectManager::startupProject();
if (!project) {
return;
}
QString projectPath = project->projectDirectory().toFSPathString();
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
QDir dir(rulesPath);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
void ChatRootView::openSettings()
{
QMetaObject::invokeMethod(
@@ -831,13 +876,12 @@ QString ChatRootView::chatTitle() const
QString ChatRootView::computeChatTitle() const
{
if (!m_chatModel)
if (!m_history)
return {};
const auto history = m_chatModel->getChatHistory();
for (const auto &msg : history) {
if (msg.role != ChatModel::User)
for (const auto &msg : m_history->messages()) {
if (msg.role() != Message::Role::User)
continue;
const QString content = msg.content.trimmed();
const QString content = msg.text().trimmed();
if (content.isEmpty())
continue;
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
@@ -993,7 +1037,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
registry->release(m_recentFilePath);
}
if (!filePath.isEmpty()) {
registry->lock(filePath);
registry->lock(filePath, ownerSession());
}
}
@@ -1005,11 +1049,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString()));
return true;
@@ -1061,71 +1101,9 @@ QString ChatRootView::lastErrorMessage() const
return m_lastErrorMessage;
}
QVariantList ChatRootView::activeRules() const
{
return m_activeRules;
}
int ChatRootView::activeRulesCount() const
{
return m_activeRules.size();
}
QString ChatRootView::getRuleContent(int index)
{
if (index < 0 || index >= m_activeRules.size())
return QString();
return PluginLLMCore::RulesLoader::loadRuleFileContent(
m_activeRules[index].toMap()["filePath"].toString());
}
void ChatRootView::refreshRules()
{
m_activeRules.clear();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (!project) {
emit activeRulesChanged();
emit activeRulesCountChanged();
return;
}
auto ruleFiles
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
for (const auto &ruleFile : ruleFiles) {
QVariantMap ruleMap;
ruleMap["filePath"] = ruleFile.filePath;
ruleMap["fileName"] = ruleFile.fileName;
ruleMap["category"] = ruleFile.category;
m_activeRules.append(ruleMap);
}
emit activeRulesChanged();
emit activeRulesCountChanged();
}
bool ChatRootView::useTools() const
{
return Settings::chatAssistantSettings().enableChatTools();
}
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();
return m_agentController->currentSupportsTools();
}
void ChatRootView::applyFileEdit(const QString &editId)
@@ -1158,11 +1136,6 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
m_fileEditController->undoAllForCurrentMessage();
}
void ChatRootView::updateCurrentMessageEditsStats()
{
m_fileEditController->updateStats();
}
int ChatRootView::currentMessageTotalEdits() const
{
return m_fileEditController->totalEdits();
@@ -1188,14 +1161,6 @@ QString ChatRootView::lastInfoMessage() const
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
{
for (const QString &filePath : attachments) {
@@ -1214,64 +1179,17 @@ bool ChatRootView::isImageFile(const QString &filePath) const
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);
}
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();
return !configuredCompressionAgent().isEmpty();
}
void ChatRootView::compressCurrentChat()
@@ -1288,9 +1206,15 @@ void ChatRootView::compressCurrentChat()
return;
}
const QString compressionAgent = configuredCompressionAgent();
if (compressionAgent.isEmpty())
return;
autosave();
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
m_chatCompressor->setSessionManager(sessionManager());
m_chatCompressor->setActiveAgent(compressionAgent);
m_chatCompressor->startCompression(m_recentFilePath, m_history);
}
void ChatRootView::cancelCompression()

View File

@@ -1,5 +1,6 @@
// 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
@@ -10,18 +11,23 @@
#include "ChatFileManager.hpp"
#include "ChatModel.hpp"
#include "ClientInterface.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist {
class AgentFactory;
class SessionManager;
class ConversationHistory;
class Session;
}
namespace QodeAssist::Chat {
class ChatCompressor;
class AgentRoleController;
class ChatConfigurationController;
class ChatAgentController;
class FileEditController;
class InputTokenCounter;
class ChatHistoryStore;
@@ -31,7 +37,6 @@ class ChatRootView : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
@@ -45,24 +50,17 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool canCompress READ canCompress NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
@@ -73,7 +71,6 @@ public:
~ChatRootView() override;
ChatModel *chatModel() const;
QString currentTemplate() const;
void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath);
@@ -98,9 +95,10 @@ public:
Q_INVOKABLE void showAddImageDialog();
Q_INVOKABLE bool isImageFile(const QString &filePath) const;
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE bool isSendShortcut(int key, int modifiers) const;
QString sendShortcutText() const;
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath);
@@ -135,18 +133,10 @@ public:
void setRequestProgressStatus(bool state);
QString lastErrorMessage() const;
QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
bool useTools() const;
void setUseTools(bool enabled);
bool useThinking() const;
void setUseThinking(bool enabled);
Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId);
@@ -155,25 +145,15 @@ public:
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats();
Q_INVOKABLE void loadAvailableConfigurations();
Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
Q_INVOKABLE void compressCurrentChat();
Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles();
Q_INVOKABLE void applyAgentRole(const QString &roleId);
Q_INVOKABLE void openAgentRolesSettings();
QStringList availableAgentRoles() const;
QString currentAgentRole() const;
QString baseSystemPrompt() const;
QString currentAgentRoleDescription() const;
QString currentAgentRoleSystemPrompt() const;
Q_INVOKABLE void loadAvailableChatAgents();
QStringList availableChatAgents() const;
QString currentChatAgent() const;
void setCurrentChatAgent(const QString &name);
int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const;
@@ -181,9 +161,8 @@ public:
QString lastInfoMessage() const;
bool isThinkingSupport() const;
bool isCompressing() const;
bool canCompress() const;
bool isInEditor() const;
void setInEditor(bool value);
@@ -202,7 +181,6 @@ public slots:
signals:
void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged();
void linkedFilesChanged();
void inputTokensCountChanged();
@@ -218,20 +196,13 @@ signals:
void lastErrorMessageChanged();
void lastInfoMessageChanged();
void activeRulesChanged();
void activeRulesCountChanged();
void sendShortcutTextChanged();
void useToolsChanged();
void useThinkingChanged();
void currentMessageEditsStatsChanged();
void isThinkingSupportChanged();
void availableConfigurationsChanged();
void currentConfigurationChanged();
void availableAgentRolesChanged();
void currentAgentRoleChanged();
void baseSystemPromptChanged();
void availableChatAgentsChanged();
void currentChatAgentChanged();
void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath);
@@ -251,25 +222,24 @@ private:
bool deferSendForAutoCompress(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
const QStringList &linkedFiles);
void dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
const QStringList &linkedFiles);
QString configuredCompressionAgent() const;
bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const;
AgentFactory *agentFactory() const;
SessionManager *sessionManager() const;
QodeAssist::Session *ownerSession();
QodeAssist::ConversationHistory *m_history;
ChatModel *m_chatModel;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath;
QStringList m_attachmentFiles;
QStringList m_linkedFiles;
@@ -278,8 +248,6 @@ private:
QString message;
QStringList attachments;
QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false;
};
PendingSend m_pendingSend;
@@ -289,13 +257,11 @@ private:
QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress;
QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_lastInfoMessage;
ChatCompressor *m_chatCompressor;
AgentRoleController *m_agentRoleController;
ChatConfigurationController *m_configurationController;
ChatAgentController *m_agentController;
FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore;
@@ -303,6 +269,8 @@ private:
mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false;
mutable QPointer<AgentFactory> m_agentFactory;
mutable QPointer<SessionManager> m_sessionManager;
};
} // namespace QodeAssist::Chat

View File

@@ -1,10 +1,12 @@
// 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 "ChatSerializer.hpp"
#include "Logger.hpp"
#include <QBuffer>
#include <memory>
#include <QDir>
#include <QFile>
#include <QFileInfo>
@@ -12,12 +14,57 @@
#include <QJsonDocument>
#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 {
const QString ChatSerializer::VERSION = "0.2";
namespace {
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
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(),
/*autoApply=*/false,
/*isFromHistory=*/true);
}
} // namespace
const QString ChatSerializer::VERSION = "0.3";
SerializationResult ChatSerializer::saveToFile(
const ConversationHistory *history, const QString &filePath)
{
if (!history)
return {false, "No conversation history"};
if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"};
}
@@ -27,9 +74,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
}
QJsonObject root = serializeChat(model, filePath);
QJsonDocument doc(root);
QJsonDocument doc(serializeChat(history));
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
}
@@ -37,8 +82,12 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {true, QString()};
}
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
SerializationResult ChatSerializer::loadFromFile(
ConversationHistory *history, const QString &filePath)
{
if (!history)
return {false, "No conversation history"};
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
@@ -50,180 +99,140 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
return {false, QString("JSON parse error: %1").arg(error.errorString())};
}
QJsonObject root = doc.object();
QString version = root["version"].toString();
const QJsonObject root = doc.object();
const QString version = root["version"].toString();
if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)};
}
if (!deserializeChat(model, root, filePath)) {
return {false, "Failed to deserialize chat data"};
}
return {true, QString()};
if (version == VERSION)
return loadCurrent(history, root);
return loadLegacy(history, root);
}
QJsonObject ChatSerializer::serializeMessage(
const ChatModel::Message &message, const QString &chatFilePath)
{
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)
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
{
QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) {
messagesArray.append(serializeMessage(message, chatFilePath));
}
for (const auto &message : history->messages())
messagesArray.append(MessageSerializer::toJson(message));
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
return root;
}
bool ChatSerializer::deserializeChat(
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
{
QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
history->clear();
for (const auto &messageValue : messagesArray) {
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));
}
model->clear();
registerHistoricalFileEdits(history);
return {true, QString()};
}
model->setLoadingFromHistory(true);
SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root)
{
history->clear();
for (const auto &message : messages) {
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);
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()) {
history->append(std::move(assistant));
history->append(std::move(toolResults));
}
continue;
}
++i;
if (role == LegacyRole::FileEdit)
continue; // derived from the tool result in the new model
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));
}
history->append(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()));
}
history->append(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));
history->append(std::move(message));
}
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
.arg(message.images.size())
.arg(message.isRedacted)
.arg(message.signature.length()));
}
model->setLoadingFromHistory(false);
registerHistoricalFileEdits(history);
return {true, QString()};
}
return true;
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)
@@ -235,18 +244,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
bool ChatSerializer::validateVersion(const QString &version)
{
if (version == VERSION) {
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;
return version == VERSION || version == "0.2" || version == "0.1";
}
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
@@ -302,10 +300,20 @@ bool ChatSerializer::saveContentToStorage(
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);
QString fullPath = QDir(contentFolder).filePath(storedPath);
const QString contentFolder = getChatContentFolder(chatFilePath);
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);
if (!file.open(QIODevice::ReadOnly)) {
@@ -313,10 +321,12 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
return QString();
}
QByteArray contentData = file.readAll();
file.close();
const QString base64 = QString::fromUtf8(file.readAll().toBase64());
return contentData.toBase64();
if (cache)
cache->insert(fullPath, {info.lastModified(), info.size(), base64});
return base64;
}
} // namespace QodeAssist::Chat

View File

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

View File

@@ -1,5 +1,6 @@
// 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 "ChatUtils.h"

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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 "ChatView.hpp"

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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 "ChatWidget.hpp"

View File

@@ -1,5 +1,6 @@
// 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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,6 @@
// 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 "FileEditController.hpp"
@@ -9,15 +10,13 @@
#include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h>
#include "ChatModel.hpp"
#include "Logger.hpp"
#include "context/ChangesManager.h"
namespace QodeAssist::Chat {
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
FileEditController::FileEditController(QObject *parent)
: QObject(parent)
, m_chatModel(chatModel)
{
auto &changes = Context::ChangesManager::instance();
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
@@ -79,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
emit infoMessage(QString("File edit applied successfully"));
updateFileEditStatus(editId, "applied");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
@@ -94,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
emit infoMessage(QString("File edit rejected"));
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
@@ -109,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
emit infoMessage(QString("File edit undone successfully"));
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
@@ -162,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
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()
{
if (m_currentRequestId.isEmpty()) {
@@ -222,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
: 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();
}
@@ -254,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
: 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();
}

View File

@@ -1,5 +1,6 @@
// 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
@@ -8,14 +9,12 @@
namespace QodeAssist::Chat {
class ChatModel;
class FileEditController : public QObject
{
Q_OBJECT
public:
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
explicit FileEditController(QObject *parent = nullptr);
void setCurrentRequestId(const QString &requestId);
void clearCurrentRequestId();
@@ -40,9 +39,6 @@ signals:
void errorOccurred(const QString &error);
private:
void updateFileEditStatus(const QString &editId, const QString &status);
ChatModel *m_chatModel;
QString m_currentRequestId;
int m_totalEdits{0};
int m_appliedEdits{0};

View File

@@ -1,5 +1,6 @@
// 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 "FileItem.hpp"

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FileMentionItem.hpp"
@@ -87,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()
{
m_searchResults.clear();

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
@@ -28,7 +29,6 @@ public:
Q_INVOKABLE void refreshSearch();
Q_INVOKABLE void moveUp();
Q_INVOKABLE void moveDown();
Q_INVOKABLE void selectCurrent();
Q_INVOKABLE void dismiss();
Q_INVOKABLE QVariantMap handleFileSelection(

View File

@@ -1,52 +1,23 @@
// 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 "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/TokenUtils.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
namespace QodeAssist::Chat {
InputTokenCounter::InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
: QObject(parent)
, m_chatModel(chatModel)
, m_history(history)
, 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();
}
@@ -73,33 +44,8 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
recompute();
}
void InputTokenCounter::rewireToolsChangedConnection()
{
if (m_toolsChangedConn)
QObject::disconnect(m_toolsChangedConn);
m_toolsChangedConn = {};
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider)
return;
auto *tm = provider->toolsManager();
if (!tm)
return;
m_toolsChangedConn = connect(
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
}
void InputTokenCounter::recompute()
{
int inputTokens = m_messageTokens;
auto &settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
}
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
int imageTokens = 0;
for (const QString &p : paths) {
@@ -111,15 +57,24 @@ void InputTokenCounter::recompute()
return imageTokens;
};
int pendingTokens = m_messageTokens;
if (!m_attachments.isEmpty()) {
QStringList textPaths;
inputTokens += splitImageEstimate(m_attachments, textPaths);
pendingTokens += splitImageEstimate(m_attachments, textPaths);
if (!textPaths.isEmpty()) {
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()) {
QStringList textPaths;
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
@@ -129,54 +84,32 @@ void InputTokenCounter::recompute()
}
}
const auto &history = m_chatModel->getChatHistory();
for (const auto &message : history) {
inputTokens += Context::TokenUtils::estimateTokens(message.content);
inputTokens += 4; // + role
}
if (settings.enableChatTools()) {
const auto providerName = Settings::generalSettings().caProvider();
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
providerName)) {
if (auto *tm = provider->toolsManager()) {
const QJsonArray toolDefs = tm->getToolsDefinitions();
if (!toolDefs.isEmpty()) {
const QByteArray serialized
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
inputTokens += static_cast<int>(serialized.size() / 4);
}
}
if (m_history) {
for (const auto &message : m_history->messages()) {
inputTokens += Context::TokenUtils::estimateTokens(message.text());
inputTokens += 4; // + role
}
}
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
m_inputTokens = inputTokens;
emit inputTokensChanged();
}
void InputTokenCounter::recordSent()
void InputTokenCounter::recordServerUsage(int promptTokens, int cachedTokens)
{
m_lastSentEstimate = m_calibrationFactor > 0.0
? static_cast<int>(m_inputTokens / m_calibrationFactor)
: m_inputTokens;
}
void InputTokenCounter::recordServerUsage(int promptTokens)
{
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
const int serverInput = promptTokens + cachedTokens;
if (serverInput <= 0)
return;
const double rawFactor
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
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));
m_serverInputTokens = serverInput;
m_hasServerUsage = true;
recompute();
}
void InputTokenCounter::resetServerUsage()
{
m_serverInputTokens = 0;
m_hasServerUsage = false;
recompute();
}

View File

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

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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 "SessionFileRegistry.hpp"
@@ -7,29 +8,43 @@
#include <QFileInfo>
#include <Session.hpp>
namespace QodeAssist::Chat {
SessionFileRegistry::SessionFileRegistry(QObject *parent)
: QObject(parent)
{}
SessionFileRegistry::~SessionFileRegistry() = default;
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;
}
m_lockedPaths.insert(path);
const auto owner = m_locks.value(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;
}
void SessionFileRegistry::release(const QString &path)
{
m_lockedPaths.remove(path);
m_locks.remove(path);
}
void SessionFileRegistry::setPendingChatFile(const QString &path)
@@ -44,7 +59,7 @@ QString SessionFileRegistry::takePendingChatFile()
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
{
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
if (desiredPath.isEmpty() || !isLocked(desiredPath)) {
return desiredPath;
}
@@ -58,7 +73,7 @@ QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
if (!suffix.isEmpty()) {
candidate += '.' + suffix;
}
if (!m_lockedPaths.contains(candidate)) {
if (!isLocked(candidate)) {
return candidate;
}
}

View File

@@ -1,26 +1,34 @@
// 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 <QHash>
#include <QObject>
#include <QSet>
#include <QPointer>
#include <QString>
namespace QodeAssist {
class Session;
}
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.
// Shared registry mapping each chat (autosave) file to the live Session that owns it, so a
// file is busy only while its owning Session is alive (a destroyed Session frees it — the
// QPointer goes null). Keeps two chat views from autosaving into the same path.
class SessionFileRegistry : public QObject
{
Q_OBJECT
public:
explicit SessionFileRegistry(QObject *parent = nullptr);
~SessionFileRegistry() override;
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);
QString uniqueFreePath(const QString &desiredPath) const;
@@ -31,7 +39,7 @@ public:
QString takePendingChatFile();
private:
QSet<QString> m_lockedPaths;
QHash<QString, QPointer<QodeAssist::Session>> m_locks;
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

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3L22 20H2L12 3Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<path d="M12 10V14" stroke="black" stroke-width="2" stroke-linecap="round"/>
<path d="M12 17H12.01" stroke="black" stroke-width="2.4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 350 B

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-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
@@ -19,6 +20,9 @@ ChatRootView {
colorGroup: SystemPalette.Active
}
property bool hasActiveError: false
readonly property color errorColor: "#d32f2f"
palette {
window: sysPalette.window
windowText: sysPalette.windowText
@@ -114,7 +118,6 @@ ChatRootView {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
}
openChatHistory.onClicked: root.openChatHistoryFolder()
contextButton.onClicked: contextViewer.open()
pinButton {
visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
@@ -134,43 +137,18 @@ ChatRootView {
relocateTooltip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor tab")
: 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()
configSelector {
model: root.availableConfigurations
displayText: root.currentConfiguration
agentSelector {
model: root.availableChatAgents
displayText: root.currentChatAgent
onActivated: function(index) {
if (index > 0) {
root.applyConfiguration(root.availableConfigurations[index])
}
root.currentChatAgent = root.availableChatAgents[index]
}
Component.onCompleted: root.loadAvailableChatAgents()
popup.onAboutToShow: {
root.loadAvailableConfigurations()
}
}
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
root.loadAvailableChatAgents()
}
}
}
@@ -411,11 +389,10 @@ ChatRootView {
QQC.TextArea {
id: messageInput
placeholderText: Qt.platform.os === "osx"
? qsTr("Type your message here... (⌘+↩ to send)")
: qsTr("Type your message here... (Ctrl+Enter to send)")
placeholderText: qsTr("Type your message here... (%1 to send)").arg(root.sendShortcutText)
placeholderTextColor: palette.mid
color: palette.text
wrapMode: TextArea.Wrap
background: Rectangle {
radius: 2
color: palette.base
@@ -494,6 +471,9 @@ ChatRootView {
skillCommandPopup.dismiss()
event.accepted = true
}
} else if (root.isSendShortcut(event.key, event.modifiers)) {
root.sendChatMessage()
event.accepted = true
}
}
@@ -586,13 +566,25 @@ ChatRootView {
Layout.preferredHeight: 40
isCompressing: root.isCompressing
isProcessing: root.isRequestInProgress
canCompress: root.canCompress
canSend: root.currentChatAgent !== ""
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest()
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
sendButtonTooltip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
: qsTr("Stop")
sendButton.icon.source: root.isRequestInProgress
? ""
: (root.hasActiveError ? "qrc:/qt/qml/ChatView/icons/warning-icon.svg"
: "qrc:/qt/qml/ChatView/icons/chat-icon.svg")
sendButton.text: root.isRequestInProgress ? qsTr("Stop") : qsTr("Send")
sendButton.accentColor: (root.hasActiveError && !root.isRequestInProgress)
? root.errorColor : "transparent"
sendButtonTooltip.text: root.isRequestInProgress
? qsTr("Stop")
: (root.currentChatAgent === ""
? qsTr("Assign a chat agent in the Pipelines settings")
: (root.hasActiveError
? root.lastErrorMessage
: qsTr("Send message to LLM %1").arg(root.sendShortcutText)))
compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles {
@@ -667,6 +659,7 @@ ChatRootView {
}
function sendChatMessage() {
root.hasActiveError = false
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = ""
fileMentionPopup.clearMentions()
@@ -689,13 +682,122 @@ ChatRootView {
onAccepted: root.compressCurrentChat()
}
Toast {
id: errorToast
z: 1000
Rectangle {
id: errorBanner
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
border.color: Qt.darker(infoToast.color, 1.3)
toastTextColor: "#FFFFFF"
z: 1000
visible: root.hasActiveError && root.lastErrorMessage.length > 0
width: parent.width / 2
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 10
anchors.bottomMargin: bottomBar.height + 48
height: visible ? errorRow.implicitHeight + 12 : 0
color: Qt.rgba(0.83, 0.18, 0.18, 0.96)
radius: 6
border.color: Qt.darker(color, 1.3)
border.width: 1
RowLayout {
id: errorRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 6
spacing: 8
TextEdit {
Layout.fillWidth: true
text: root.lastErrorMessage
color: "#FFFFFF"
font.pixelSize: 12
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
selectionColor: Qt.darker(errorBanner.color, 1.3)
}
Rectangle {
id: copyErrorButton
property bool copied: false
Layout.alignment: Qt.AlignTop
implicitWidth: copyErrorLabel.implicitWidth + 18
implicitHeight: 22
radius: 4
color: copyErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28)
: Qt.rgba(1, 1, 1, 0.16)
border.color: Qt.rgba(1, 1, 1, 0.45)
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Text {
id: copyErrorLabel
anchors.centerIn: parent
text: copyErrorButton.copied ? qsTr("Copied") : qsTr("Copy")
color: "#FFFFFF"
font.pixelSize: 12
}
MouseArea {
id: copyErrorMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.copyToClipboard(root.lastErrorMessage)
copyErrorButton.copied = true
copyErrorResetTimer.restart()
}
}
Timer {
id: copyErrorResetTimer
interval: 1500
onTriggered: copyErrorButton.copied = false
}
}
Rectangle {
id: closeErrorButton
Layout.alignment: Qt.AlignTop
implicitWidth: 22
implicitHeight: 22
radius: 4
color: closeErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28) : "transparent"
border.color: Qt.rgba(1, 1, 1, 0.45)
border.width: closeErrorMouse.containsMouse ? 1 : 0
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: "✕"
color: "#FFFFFF"
font.pixelSize: 12
}
MouseArea {
id: closeErrorMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hasActiveError = false
}
}
}
}
Toast {
@@ -707,35 +809,11 @@ ChatRootView {
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 {
target: root
function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) {
errorToast.show(root.lastErrorMessage)
root.hasActiveError = true
}
}
function onLastInfoMessageChanged() {

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import ChatView
@@ -355,10 +356,9 @@ Rectangle {
smooth: true
mipmap: true
BusyIndicator {
QoABusyIndicator {
anchors.centerIn: parent
running: imageDisplay.status === Image.Loading
visible: running
}
Text {

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-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

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import Qt.labs.platform as Platform

View File

@@ -1,5 +1,6 @@
// 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 Qt.labs.platform as Platform

View File

@@ -1,5 +1,6 @@
// 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 Qt.labs.platform as Platform

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-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
@@ -19,6 +20,9 @@ Rectangle {
property alias cancelCompressButton: cancelCompressButtonId
property bool isCompressing: false
property bool isProcessing: false
property bool canCompress: true
property bool canSend: true
property alias sendButtonTooltip: sendButtonTooltipId
color: palette.window.hslLightness > 0.5 ?
@@ -107,7 +111,7 @@ Rectangle {
visible: root.isCompressing
spacing: 6
BusyIndicator {
QoABusyIndicator {
id: compressBusyIndicator
anchors.verticalCenter: parent.verticalCenter
@@ -137,37 +141,78 @@ Rectangle {
}
}
QoAButton {
id: compressButtonId
Item {
id: compressButtonContainer
visible: !root.isCompressing
text: qsTr("Compress")
implicitWidth: compressButtonId.implicitWidth
implicitHeight: compressButtonId.implicitHeight
icon {
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
height: 15
width: 15
QoAButton {
id: compressButtonId
anchors.fill: parent
enabled: root.canCompress
text: qsTr("Compress")
icon {
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
height: 15
width: 15
}
}
HoverHandler {
id: compressHoverHandler
}
QoAToolTip {
visible: compressButtonId.hovered
visible: compressHoverHandler.hovered
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")
}
}
QoAButton {
id: sendButtonId
Item {
id: sendButtonContainer
icon {
height: 15
width: 15
implicitWidth: sendButtonId.implicitWidth
implicitHeight: sendButtonId.implicitHeight
QoAButton {
id: sendButtonId
anchors.fill: parent
enabled: root.isProcessing || root.canSend
leftPadding: root.isProcessing ? 22 : 4
icon {
height: 15
width: 15
}
QoABusyIndicator {
id: sendBusyIndicator
anchors.left: parent.left
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
width: 14
height: 14
running: root.isProcessing
}
}
HoverHandler {
id: sendHoverHandler
}
QoAToolTip {
id: sendButtonTooltipId
visible: sendButtonId.hovered
visible: sendHoverHandler.hovered
delay: 250
}
}

View File

@@ -1,542 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
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

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 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

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-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.Layouts
@@ -21,12 +22,8 @@ Rectangle {
property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId
property alias roleSelector: roleSelector
property alias agentSelector: agentSelectorId
property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ?
@@ -133,7 +130,7 @@ Rectangle {
}
QoAComboBox {
id: configSelectorId
id: agentSelectorId
implicitHeight: 25
@@ -141,87 +138,17 @@ Rectangle {
currentIndex: 0
QoAToolTip {
visible: configSelectorId.hovered
visible: agentSelectorId.hovered
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 {
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 {
id: settingsButtonId
@@ -331,23 +258,6 @@ Rectangle {
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 {
id: tokensBadgeId

View File

@@ -1,6 +1,7 @@
// Copyright (C) 2024-2026 Petr Mironychev
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>
@@ -208,24 +209,4 @@ QString CodeHandler::detectLanguageFromExtension(const QString &extension)
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

View File

@@ -1,5 +1,6 @@
// 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
@@ -31,10 +32,6 @@ public:
private:
static QString getCommentPrefix(const QString &language);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex();
};
} // namespace QodeAssist

View File

@@ -1,235 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,47 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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

21
LICENSE
View File

@@ -1,3 +1,24 @@
===============================================================
ADDITIONAL TERMS UNDER GPLv3 SECTION 7(b)
===============================================================
In accordance with Section 7(b) of the GNU General Public License v3.0,
the following additional attribution term applies to QodeAssist:
You must preserve all author attributions, copyright notices, and the
project name "QodeAssist" in all copies and modified versions,
including in source file headers, the plugin metadata
(QodeAssist.json.in), and the About dialog or equivalent user-facing
identification. Modified versions must be clearly marked as different
from the original.
This is a reasonable attribution requirement permitted under GPLv3
§7(b) and §7(c). It supplements the notice-preservation obligations of
§4 and §5.
Copyright (C) 2024-2026 Petr Mironychev
===============================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

View File

@@ -1,39 +1,61 @@
// 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 "LLMClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QDir>
#include <QFileInfo>
#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 "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include <pluginllmcore/RulesLoader.hpp>
#include "sources/settings/PipelinesConfig.hpp"
namespace QodeAssist {
LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry,
PluginLLMCore::IPromptProvider *promptProvider,
AgentFactory &agentFactory,
SessionManager &sessionManager,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_agentFactory(agentFactory)
, m_sessionManager(sessionManager)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this))
{
}
{}
LLMClientInterface::~LLMClientInterface()
{
@@ -50,58 +72,56 @@ void LLMClientInterface::startImpl()
emit started();
}
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
void LLMClientInterface::onCompletionFinished(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
sendCompletionToClient(fullText, ctx.originalRequest, true);
QString fullText;
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);
m_performanceLogger.endTimeMeasurement(requestId);
sendCompletionToClient(fullText, originalRequest, true);
finishRequest(requestId);
}
void LLMClientInterface::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId) || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
finishRequest(requestId);
}
void LLMClientInterface::finishRequest(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
if (session)
m_sessionManager.release(session);
}
void LLMClientInterface::sendData(const QByteArray &data)
@@ -134,26 +154,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
void LLMClientInterface::handleCancelRequest()
{
QSet<PluginLLMCore::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
const auto requests = m_activeRequests;
m_activeRequests.clear();
for (auto it = requests.begin(); it != requests.end(); ++it) {
m_performanceLogger.endTimeMeasurement(it.key());
if (Session *session = it.value().session)
m_sessionManager.release(session);
}
LOG_MESSAGE("All requests cancelled and state cleared");
}
@@ -192,34 +201,19 @@ void LLMClientInterface::handleShutdown(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)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = errorMessage;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
// End performance measurement if it was started
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
@@ -236,130 +230,103 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return;
}
auto updatedContext = prepareContext(request, documentInfo);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
const QString agentName = pickCompletionAgent(filePath);
if (agentName.isEmpty()) {
QString error = QString("No code completion agent matches: %1").arg(filePath);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
}
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
QString sessionError;
Session *session = m_sessionManager.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) {
QString error = QString("No template found with name: %1").arg(templateName);
Templates::ContextData context = prepareContext(request, documentInfo);
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);
sendErrorResponse(request, error);
return;
}
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for completion");
}
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<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);
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_activeRequests[requestId] = {request, session};
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)
{
QJsonObject params = request["params"].toObject();
@@ -373,14 +340,6 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
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
{
return m_contextManager;
@@ -389,15 +348,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response;
@@ -416,14 +366,13 @@ void LLMClientInterface::sendCompletionToClient(
if (outputHandler == "Raw text") {
processedCompletion = completion;
} else if (outputHandler == "Force processing") {
processedCompletion = CodeHandler::processText(completion,
Context::extractFilePathFromRequest(request));
processedCompletion
= CodeHandler::processText(completion, Context::extractFilePathFromRequest(request));
} else { // "Auto"
processedCompletion = CodeHandler::hasCodeBlocks(completion)
? CodeHandler::processText(completion,
Context::extractFilePathFromRequest(
request))
: completion;
processedCompletion
= CodeHandler::hasCodeBlocks(completion)
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
: completion;
}
if (processedCompletion.endsWith('\n')) {
@@ -435,11 +384,11 @@ void LLMClientInterface::sendCompletionToClient(
}
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range;
range["start"] = position;
range["end"] = position;
completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem);

View File

@@ -1,5 +1,6 @@
// 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
@@ -7,12 +8,11 @@
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include <QPointer>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
@@ -22,6 +22,14 @@ class QNetworkAccessManager;
namespace QodeAssist {
class AgentFactory;
class Session;
class SessionManager;
namespace Templates {
struct ContextData;
}
class LLMClientInterface : public LanguageClient::BaseClientInterface
{
Q_OBJECT
@@ -30,8 +38,8 @@ public:
LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry,
PluginLLMCore::IPromptProvider *promptProvider,
AgentFactory &agentFactory,
SessionManager &sessionManager,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override;
@@ -51,37 +59,33 @@ public:
protected:
void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private:
void handleInitialize(const QJsonObject &request);
void handleShutdown(const QJsonObject &request);
void handleTextDocumentDidOpen(const QJsonObject &request);
void handleInitialized(const QJsonObject &request);
void handleExit(const QJsonObject &request);
void handleCancelRequest();
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
{
QJsonObject originalRequest;
PluginLLMCore::Provider *provider;
QPointer<Session> session;
};
PluginLLMCore::ContextData prepareContext(
Templates::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint(
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
QString pickCompletionAgent(const QString &filePath) const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
PluginLLMCore::IProviderRegistry &m_providerRegistry;
AgentFactory &m_agentFactory;
SessionManager &m_sessionManager;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer;

View File

@@ -1,6 +1,7 @@
// Copyright (C) 2023 The Qt Company Ltd.
// 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 "LLMSuggestion.hpp"
#include <texteditor/texteditor.h>

View File

@@ -20,6 +20,8 @@
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/
#pragma once

View File

@@ -20,6 +20,8 @@
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/
#pragma once
@@ -33,7 +35,6 @@ namespace QodeAssist {
class Completion : public LanguageServerProtocol::JsonObject
{
static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"};
static constexpr LanguageServerProtocol::Key uuidKey{"uuid"};
public:
using JsonObject::JsonObject;
@@ -53,7 +54,6 @@ public:
}
QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); }
void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); }
QString uuid() const { return typedValue<QString>(uuidKey); }
bool isValid() const override
{

View File

@@ -1,12 +1,12 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.9.19",
"Version" : "0.9.21",
"CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GPLv3",
"License" : "GPLv3 with additional attribution terms (§7b) — see LICENSE",
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
"Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",

View File

@@ -20,6 +20,8 @@
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/
#include "QodeAssistClient.hpp"
@@ -157,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
QodeAssistClient::~QodeAssistClient()
{
cleanupConnections();
@@ -261,9 +273,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
if (m_llmClient->contextManager()->shouldIgnore(
editor->textDocument()->filePath().toUrlishString())) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
@@ -307,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
if (m_llmClient->contextManager()->shouldIgnore(
editor->textDocument()->filePath().toUrlishString())) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
@@ -317,6 +327,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this);
m_refactorHandler->setSessionManager(m_sessionManager);
m_refactorHandler->setAgentFactory(m_agentFactory);
connect(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,

View File

@@ -1,10 +1,12 @@
// Copyright (C) 2023 The Qt Company Ltd.
// 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 "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
@@ -12,14 +14,14 @@
#include "RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
namespace QodeAssist {
class SessionManager;
class AgentFactory;
class QodeAssistClient : public LanguageClient::Client
{
Q_OBJECT
@@ -27,6 +29,9 @@ public:
explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override;
@@ -62,11 +67,12 @@ private:
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient;
SessionManager *m_sessionManager{nullptr};
AgentFactory *m_agentFactory{nullptr};
};
} // namespace QodeAssist

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,26 +1,46 @@
// 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 "QuickRefactorHandler.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolLoopRunner.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <context/DocumentContextReader.hpp>
#include <pluginllmcore/ResponseCleaner.hpp>
#include <context/DocumentReaderQtCreator.hpp>
#include <context/EnvBlockFormatter.hpp>
#include <context/Utils.hpp>
#include <pluginllmcore/PromptTemplateManager.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <pluginllmcore/RulesLoader.hpp>
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <sources/common/ResponseCleaner.hpp>
#include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp>
#include <settings/ToolsSettings.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 {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
@@ -33,6 +53,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
@@ -87,102 +117,114 @@ void QuickRefactorHandler::sendRefactorRequest(
prepareAndSendRequest(editor, instructions, range);
}
QString QuickRefactorHandler::configuredAgent(AgentFactory *agentFactory)
{
const QString configured = Settings::PipelinesConfig::load().rosters.quickRefactor;
if (configured.isEmpty() || !agentFactory || !agentFactory->configByName(configured))
return {};
return configured;
}
QString QuickRefactorHandler::pickRefactorAgent() const
{
return configuredAgent(m_agentFactory);
}
void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range)
{
auto &settings = Settings::generalSettings();
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
const auto providerName = settings.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
const auto emitError = [this, editor](const QString &error) {
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
};
if (!m_sessionManager) {
emitError(QStringLiteral("Quick refactor session manager is not available"));
return;
}
const auto templateName = settings.qrTemplate();
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (!promptTemplate) {
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
const QString agentName = pickRefactorAgent();
if (agentName.isEmpty()) {
emitError(QStringLiteral(
"No quick refactor agent configured. Set one in QodeAssist > General."));
return;
}
QJsonObject payload{
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
QString sessionError;
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();
bool enableThinking = Settings::quickRefactorSettings().useThinking();
provider->prepareRequest(
payload,
promptTemplate,
context,
PluginLLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
auto *project = ProjectExplorer::ProjectManager::startupProject();
Templates::ContextRenderer::Bindings bindings;
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
bindings.configDir = AgentFactory::userConfigDir();
session->setContextBindings(bindings);
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
const AgentConfig *agentConfig
= m_agentFactory ? m_agentFactory->configByName(agentName) : nullptr;
if (agentConfig && agentConfig->enableTools) {
m_sessionManager->toolContributors().contribute(client->tools());
client->toolLoop()->setMaxRounds(Settings::toolsSettings().maxToolContinuations());
}
session->systemPrompt()->setLayer(
QStringLiteral("refactor"), buildContextLayer(editor, range));
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_isRefactoringInProgress = true;
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
session, &Session::finished, this,
[this](const LLMQore::RequestID &id, const QString &) { onRefactorFinished(id); });
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&QuickRefactorHandler::handleRequestFinalized,
Qt::UniqueConnection);
session, &Session::failed, this,
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onRefactorFailed(id, error);
});
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&QuickRefactorHandler::handleRequestFailed,
Qt::UniqueConnection);
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
const QString userMessage = instructions.isEmpty()
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
: instructions;
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
const LLMQore::RequestID requestId = session->send(std::move(blocks));
if (requestId.isEmpty()) {
m_isRefactoringInProgress = false;
const QString reason = session->lastError().message;
m_sessionManager->removeSession(session);
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;
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider};
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
}
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
QString QuickRefactorHandler::buildContextLayer(
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
{
PluginLLMCore::ContextData context;
Q_UNUSED(range)
auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader;
@@ -190,7 +232,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available");
return context;
return {};
}
QTextCursor cursor = editor->textCursor();
@@ -264,43 +306,20 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
taggedContent = contextBefore + "<cursor>" + contextAfter;
}
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
QString contextLayer = Context::EnvBlockFormatter::formatFile(
{documentInfo.filePath, documentInfo.mimeType});
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
project, PluginLLMCore::RulesContext::QuickRefactor);
contextLayer += "\n# Code Context with Position Markers\n" + taggedContent;
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for quick refactor");
}
}
systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath;
systemPrompt += "\n\n# Code Context with Position Markers\n" + taggedContent;
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
systemPrompt += cursor.hasSelection()
contextLayer += "\n\n# What to Generate:";
contextLayer += cursor.hasSelection()
? "\n- Generate ONLY the code that should REPLACE the selected text between "
"<selection_start> and <selection_end> markers"
"\n- Your output will completely replace the selected code"
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
"\n- Your output will be inserted at the cursor location";
systemPrompt += "\n\n## Formatting Rules:"
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
"\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:";
QString indentNote;
if (cursor.hasSelection()) {
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
int leadingSpaces = 0;
@@ -310,12 +329,12 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
else break;
}
if (leadingSpaces > 0) {
systemPrompt += 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- Each line in your output must maintain this base indentation")
.arg(leadingSpaces);
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- Each line in your output must maintain this base indentation")
.arg(leadingSpaces);
}
systemPrompt += "\n- PRESERVE all indentation from the original code";
indentNote += "\n- PRESERVE all indentation from the original code";
} else {
QTextBlock block = documentInfo.document->findBlock(cursorPos);
QString lineText = block.text();
@@ -326,61 +345,20 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
else break;
}
if (leadingSpaces > 0) {
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation"
"\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);
indentNote = QString("\n- CRITICAL: Current line has %1 spaces of indentation"
"\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);
}
}
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
"\n- Maintain consistent indentation for nested blocks"
"\n- Do NOT remove or reduce the base indentation level"
"\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 (!indentNote.isEmpty())
contextLayer += "\n\n## Indentation:" + indentNote;
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
contextLayer += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
}
context.systemPrompt = systemPrompt;
QVector<PluginLLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
m_isRefactoringInProgress = false;
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
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);
}
return contextLayer;
}
void QuickRefactorHandler::cancelRequest()
@@ -394,10 +372,10 @@ void QuickRefactorHandler::cancelRequest()
auto it = m_activeRequests.find(id);
if (it != m_activeRequests.end()) {
auto provider = it.value().provider;
Session *session = it.value().session;
m_activeRequests.erase(it);
if (provider)
provider->cancelRequest(id);
if (session && m_sessionManager)
m_sessionManager->release(session);
}
RefactorResult result;
@@ -406,42 +384,66 @@ void QuickRefactorHandler::cancelRequest()
emit refactoringCompleted(result);
}
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
{
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)
if (requestId != m_lastRequestId)
return;
const auto &u = *info.usage;
LOG_MESSAGE(
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));
auto it = m_activeRequests.find(requestId);
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
if (it != m_activeRequests.end())
m_activeRequests.erase(it);
QString fullText;
if (session) {
if (auto *history = session->history(); history && !history->isEmpty())
fullText = history->messages().back().text();
}
m_isRefactoringInProgress = false;
m_lastRequestId.clear();
const QString cleanedResponse = ResponseCleaner::clean(fullText);
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
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);
if (session && m_sessionManager)
m_sessionManager->release(session);
}
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
void QuickRefactorHandler::onRefactorFailed(
const QString &requestId, const QodeAssist::ErrorInfo &error)
{
if (requestId == m_lastRequestId) {
m_activeRequests.remove(requestId);
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = m_currentEditor;
emit refactoringCompleted(result);
}
if (requestId != m_lastRequestId)
return;
auto it = m_activeRequests.find(requestId);
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
if (it != m_activeRequests.end())
m_activeRequests.erase(it);
m_isRefactoringInProgress = false;
m_lastRequestId.clear();
RefactorResult result;
result.success = false;
result.errorMessage = error.message;
result.editor = m_currentEditor;
emit refactoringCompleted(result);
if (session && m_sessionManager)
m_sessionManager->release(session);
}
} // namespace QodeAssist

View File

@@ -1,29 +1,34 @@
// 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 <QJsonObject>
#include <QObject>
#include <QPointer>
#include <LLMQore/BaseClient.hpp>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <ErrorInfo.hpp>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist {
class SessionManager;
class Session;
class AgentFactory;
struct RefactorResult
{
QString newText;
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
TextEditor::TextEditorWidget *editor{nullptr};
QPointer<TextEditor::TextEditorWidget> editor;
};
class QuickRefactorHandler : public QObject
@@ -34,40 +39,41 @@ public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
bool isProcessing() const { return m_isRefactoringInProgress; }
static QString configuredAgent(AgentFactory *agentFactory);
signals:
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:
void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
PluginLLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
void onRefactorFinished(const QString &requestId);
void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
QString buildContextLayer(
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
QString pickRefactorAgent() const;
struct RequestContext
{
QJsonObject originalRequest;
PluginLLMCore::Provider *provider;
QPointer<Session> session;
};
QPointer<SessionManager> m_sessionManager;
QPointer<AgentFactory> m_agentFactory;
QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor;
QPointer<TextEditor::TextEditorWidget> m_currentEditor;
Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress;
QString m_lastRequestId;

159
README.md
View File

@@ -41,7 +41,7 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **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
- **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!
@@ -99,7 +99,8 @@ You can install and update QodeAssist directly from within Qt Creator by adding
1. Open the Extensions page (`Qt Creator → Extensions`) and switch to the **Browser** tab
2. Enable **Use External Repository**
3. Next to **Repository URLs**, click **Add** and paste the registry archive URL matching your Qt Creator version:
- **Latest (QtC 19)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
- **Latest (up to date to QtCreator release)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
- **QtC 20**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc20.tar.gz`
- **QtC 19**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc19.tar.gz`
- **QtC 18**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc18.tar.gz`
<details>
@@ -216,9 +217,8 @@ For optimal coding assistance, we recommend using these top-tier models:
### 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
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
## Features
@@ -254,7 +254,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- Multiple chat panels: side panel, bottom panel, and popup window
- Chat history with auto-save and restore
- 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
- **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional)
@@ -348,18 +348,16 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
├─────────────────────────────────────────────────────────────────────────────┤
│ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │
│ │
│ 1. System Prompt (from Code Completion Settings - FIM variant)
2. Project Rules:
.qodeassist/rules/completion/*.md
3. Open Files Context (optional, if enabled):
└─ Currently open editor files
│ 4. Code Context: │
│ 1. Editor Context:
├─ File information (language, path)
Recent project changes (optional, if enabled)
└─ Open editor files (optional, if enabled) │
2. Code Context:
│ ├─ Code before cursor (prefix) │
│ └─ Code after cursor (suffix) │
│ │
│ Final Prompt: FIM_Template(Prefix: SystemPrompt + Rules + OpenFiles +
CodeBefore,
│ Suffix: CodeAfter) │
│ Final Request: the agent's TOML [body] template renders prefix/suffix
into the provider's native FIM fields
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -374,21 +372,19 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
├─────────────────────────────────────────────────────────────────────────────┤
│ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │
│ │
│ 1. System Prompt (from Code Completion Settings - Non-FIM variant)
│ └─ Includes response formatting instructions
│ 2. Project Rules:
│ └─ .qodeassist/rules/completion/*.md │
│ 3. Open Files Context (optional, if enabled): │
│ └─ Currently open editor files │
│ 4. Code Context: │
│ 1. Completion Instructions (from the agent's TOML profile)
│ └─ Includes response formatting rules
│ 2. Editor Context:
│ ├─ File information (language, path) │
│ ├─ Recent project changes (optional, if enabled) │
│ └─ Open editor files (optional, if enabled) │
│ 3. Code Context: │
│ ├─ Code before cursor │
│ ├─ <cursor> marker │
│ └─ Code after cursor │
│ 5. User Message: "Complete the code at cursor position" │
│ │
│ Final Prompt: [System: SystemPrompt + Rules]
│ [User: OpenFiles + Context + CompletionRequest]
│ Final Prompt: [System: Instructions + EditorContext]
│ [User: Code around cursor as a completion request] │
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -401,27 +397,22 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
┌─────────────────────────────────────────────────────────────────────────────┐
│ CHAT ASSISTANT │
├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Chat Assistant Settings)
│ 2. Agent Role (optional, from role selector):
└─ Role-specific system prompt (Developer, Reviewer, custom)
3. Project Rules:
│ ├─ .qodeassist/rules/common/*.md
.qodeassist/rules/chat/*.md
4. File Context (optional):
├─ Attached files (manual)
Linked files (persistent)
└─ Open editor files (if auto-sync enabled)
5. Tool Definitions (if enabled):
├─ ReadProjectFileByName
│ ├─ ListProjectFiles │
│ ├─ SearchInProject │
│ └─ GetIssuesList │
│ 6. Conversation History │
│ 7. User Message │
│ 1. Agent System Prompt (persona, from the agent's TOML profile)
│ 2. Project Info + Skills (catalog and always-on skills)
3. Tool Definitions (if the agent enables tools)
4. Conversation History:
│ ├─ Previous messages and tool calls/results
Attachments stay with the message they were sent with
└─ /skill instructions persist for the whole conversation
5. Linked Files + Open Editor Files (if auto-sync enabled):
FRESH snapshot of current file content, re-read on every
request and placed next to your latest message — never
duplicated into the history
6. User Message (+ this turn's attachments and images)
│ │
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools]
│ Final Prompt: [System: Persona + ProjectInfo + Skills]
│ [History: Previous messages] │
│ [User: FileContext + UserMessage]
│ [User: CurrentFilesSnapshot + UserMessage + Attachments]
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -434,28 +425,26 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUICK REFACTORING │
├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Quick Refactor Settings)
│ 2. Project Rules:
│ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/quickrefactor/*.md │
│ 3. Code Context: │
│ 1. Agent System Prompt (persona, from the agent's TOML profile)
│ 2. Code Context (generated):
│ ├─ File information (language, path) │
│ ├─ Code before selection (configurable amount) │
│ ├─ <selection_start> marker │
│ ├─ Selected code (or current line) │
│ ├─ <selection_end> marker │
│ ├─ <cursor> marker (position within selection) │
─ Code after selection (configurable amount) │
4. Refactor Instruction:
─ Code after selection (configurable amount) │
└─ Output formatting and indentation rules
│ 3. Refactor Instruction (the user message): │
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
│ ├─ Custom Instruction (from library) │
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
│ │ quick_refactor/instructions/*.json │
│ └─ Additional Details (optional user input) │
5. Tool Definitions (if enabled)
4. Tool Definitions (if the agent enables tools)
│ │
│ Final Prompt: [System: SystemPrompt + Rules]
│ [User: Context + Markers + Instruction + Details] │
│ Final Prompt: [System: Persona + CodeContext + Rules] │
│ [User: Instruction + Details]
└─────────────────────────────────────────────────────────────────────────────┘
```
@@ -463,17 +452,15 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
### Key Points
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
- **System Prompts** are configured independently for each feature in Settings
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
- **FIM vs Non-FIM models** for code completion use different System Prompts:
- FIM models: Direct completion prompt
- Non-FIM models: Prompt includes response formatting instructions
- **Quick Refactor** has its own provider/model configuration, independent from Chat
- **System Prompts** live in the agent's TOML profile (`system_prompt`); switch personas by switching agents
- **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
- **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** 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
- **Quick Refactor** has its own agent roster, independent from Chat
- **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
@@ -521,7 +508,6 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
- [x] Diff sharing with models
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
- [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 — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
- [ ] Full project source sharing
@@ -532,7 +518,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.
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.
@@ -580,6 +566,10 @@ cmake --build .
## 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
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
@@ -588,7 +578,46 @@ cmake --build .
### 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
QodeAssist is licensed under the **GNU General Public License v3.0**
(see [`LICENSE`](LICENSE)), with **additional attribution terms under
GPLv3 Section 7(b)**.
You are free to use, modify, and redistribute QodeAssist under GPL-3.0,
but you **must preserve** the original author attribution, copyright
notices, and project identification — including in source file headers,
the plugin metadata (`QodeAssist.json.in`), and the About dialog or
equivalent user-facing identification. Modified versions must be clearly
marked as different from the original.
### Commercial licensing
QodeAssist is also available under a separate commercial license for use
in proprietary or closed-source products without GPL-3.0 obligations.
For commercial licensing inquiries, contact **palm1r-github-dev@pm.me**.
### Qt Creator components and attributions
QodeAssist is a plugin for Qt Creator and incorporates certain components
(plugin templates, API headers, and related boilerplate) originating from
Qt Creator, which are copyright (C) The Qt Company Ltd.
These components are provided by The Qt Company under the GNU General
Public License version 3, annotated with **The Qt Company GPL Exception
1.0**. This exception permits the development and distribution of Qt
Creator plugins under licenses of the plugin author's own choosing,
notwithstanding the GPL's general linking requirements. It is this
exception that allows QodeAssist to be offered under both GPL-3.0 and a
separate commercial license.
The original copyright and license notices of The Qt Company are
preserved in the relevant source files and must not be removed.
For Qt Creator's licensing terms, see
[LICENSE.GPL3-EXCEPT](https://github.com/qt-creator/qt-creator/blob/master/LICENSES/LICENSE.GPL3-EXCEPT).
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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 "RefactorSuggestion.hpp"
#include "LLMSuggestion.hpp"

View File

@@ -1,5 +1,6 @@
// 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

View File

@@ -1,5 +1,6 @@
// 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 "RefactorSuggestionHoverHandler.hpp"
#include "RefactorSuggestion.hpp"

View File

@@ -1,5 +1,6 @@
// 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
@@ -31,7 +32,6 @@ public:
void setSuggestionRange(const Utils::Text::Range &range);
void clearSuggestionRange();
bool hasSuggestion() const { return m_hasSuggestion; }
void setApplyCallback(ApplyCallback callback) { m_applyCallback = 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,104 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,70 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,90 +0,0 @@
#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,61 +0,0 @@
#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,54 +0,0 @@
#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,31 +0,0 @@
#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,82 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,41 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,153 +0,0 @@
#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,55 +0,0 @@
#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,29 +0,0 @@
#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,25 +0,0 @@
#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,69 +0,0 @@
#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,49 +0,0 @@
#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

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