Compare commits

..

29 Commits

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

View File

@@ -27,7 +27,7 @@ jobs:
config: config:
- { - {
name: "Windows Latest MSVC", artifact: "Windows-x64", name: "Windows Latest MSVC", artifact: "Windows-x64",
os: windows-latest, os: windows-2022,
platform: windows_x64, platform: windows_x64,
cc: "cl", cxx: "cl", cc: "cl", cxx: "cl",
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat", 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_version: "6.10.3",
qt_creator_version: "19.0.2" qt_creator_version: "19.0.2"
} }
- {
qt_version: "6.11.1",
qt_creator_version: "20.0.0"
}
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
@@ -110,10 +114,14 @@ jobs:
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}") set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
string(REPLACE "." "" qt_version_dotless "${qt_version}") string(REPLACE "." "" qt_version_dotless "${qt_version}")
set(qt_repo_dir "qt6_${qt_version_dotless}")
if ("${{ runner.os }}" STREQUAL "Windows") if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86") set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_msvc2022_64") set(qt_package_arch_suffix "win64_msvc2022_64")
set(qt_dir_prefix "${qt_version}/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") 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") set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
else() else()
@@ -127,7 +135,9 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64") set(qt_package_arch_suffix "linux_gcc_64")
endif() endif()
set(qt_dir_prefix "${qt_version}/gcc_64") 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") set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
else() else()
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64") set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
@@ -143,7 +153,7 @@ jobs:
endif() endif()
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(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
file(READ ./Updates.xml updates_xml) file(READ ./Updates.xml updates_xml)
@@ -170,7 +180,11 @@ jobs:
) )
endforeach() 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( downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z" "${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z ${package}.7z
@@ -236,7 +250,7 @@ jobs:
endif() endif()
set(build_plugin_py "scripts/build_plugin.py") 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") if(EXISTS "${{ steps.qt_creator.outputs.qtc_dir }}/${dir}/build_plugin.py")
set(build_plugin_py "${dir}/build_plugin.py") set(build_plugin_py "${dir}/build_plugin.py")
break() break()

View File

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

View File

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

View File

@@ -0,0 +1,114 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatAgentController.hpp"
#include <QSettings>
#include <coreplugin/icore.h>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <sources/settings/PipelinesConfig.hpp>
namespace QodeAssist::Chat {
namespace {
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
}
ChatAgentController::ChatAgentController(QObject *parent)
: QObject(parent)
{
if (auto *settings = Core::ICore::settings())
m_currentAgent = settings->value(kChatAgentKey).toString();
connect(
Settings::PipelinesNotifier::instance(),
&Settings::PipelinesNotifier::pipelinesChanged,
this,
&ChatAgentController::reload);
}
void ChatAgentController::setAgentFactory(AgentFactory *factory)
{
m_agentFactory = factory;
if (factory)
connect(
factory,
&AgentFactory::agentsChanged,
this,
&ChatAgentController::reload,
Qt::UniqueConnection);
reload();
}
QStringList ChatAgentController::availableAgents() const
{
return m_availableAgents;
}
QString ChatAgentController::currentAgent() const
{
return m_currentAgent;
}
void ChatAgentController::setCurrentAgent(const QString &name)
{
if (name == m_currentAgent || !m_availableAgents.contains(name))
return;
applyCurrentAgent(name);
}
void ChatAgentController::reload()
{
if (!m_agentFactory)
return;
const QStringList all = m_agentFactory->configNames();
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatAssistant;
QStringList filtered;
for (const QString &name : roster) {
if (all.contains(name))
filtered.append(name);
}
if (filtered != m_availableAgents) {
m_availableAgents = filtered;
emit availableAgentsChanged();
}
ensureValidCurrent();
}
void ChatAgentController::ensureValidCurrent()
{
if (m_availableAgents.contains(m_currentAgent))
return;
const QString next = m_availableAgents.isEmpty() ? QString() : m_availableAgents.first();
if (next == m_currentAgent)
return;
applyCurrentAgent(next);
}
void ChatAgentController::applyCurrentAgent(const QString &name)
{
m_currentAgent = name;
if (auto *settings = Core::ICore::settings())
settings->setValue(kChatAgentKey, m_currentAgent);
emit currentAgentChanged();
}
bool ChatAgentController::currentSupportsTools() const
{
if (!m_agentFactory || m_currentAgent.isEmpty())
return false;
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
return config && config->enableTools;
}
} // namespace QodeAssist::Chat

View File

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

View File

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

View File

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

View File

@@ -1,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 // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatFileManager.hpp" #include "ChatFileManager.hpp"
#include "Logger.hpp" #include "Logger.hpp"

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatHistoryStore.hpp" #include "ChatHistoryStore.hpp"
@@ -15,14 +16,22 @@
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent) ChatHistoryStore::ChatHistoryStore(
ConversationHistory *history, ChatModel *chatModel, QObject *parent)
: QObject(parent) : QObject(parent)
, m_history(history)
, m_chatModel(chatModel) , m_chatModel(chatModel)
{} {}
@@ -51,17 +60,23 @@ QString ChatHistoryStore::suggestedFileName() const
{ {
QString shortMessage; QString shortMessage;
if (m_chatModel->rowCount() > 0) { if (m_history) {
QString firstMessage for (const auto &message : m_history->messages()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString(); if (message.role() != Message::Role::User)
shortMessage = firstMessage.split('\n').first().simplified().left(30); continue;
if (shortMessage.isEmpty()) { const QString text = message.text();
QVariantList images if (!text.trimmed().isEmpty()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList(); shortMessage = text.split('\n').first().simplified().left(30);
if (!images.isEmpty()) { } else {
shortMessage = "image_chat"; for (const auto &block : message.blocks()) {
if (dynamic_cast<StoredImageContent *>(block.get())) {
shortMessage = "image_chat";
break;
}
}
} }
break;
} }
} }
@@ -106,12 +121,17 @@ QString ChatHistoryStore::autosaveFilePath(
SerializationResult ChatHistoryStore::save(const QString &filePath) const SerializationResult ChatHistoryStore::save(const QString &filePath) const
{ {
return ChatSerializer::saveToFile(m_chatModel, filePath); return ChatSerializer::saveToFile(
m_history, filePath, m_chatModel ? m_chatModel->usageToJson() : QJsonObject{});
} }
SerializationResult ChatHistoryStore::load(const QString &filePath) const SerializationResult ChatHistoryStore::load(const QString &filePath) const
{ {
return ChatSerializer::loadFromFile(m_chatModel, filePath); QJsonObject usage;
const SerializationResult result = ChatSerializer::loadFromFile(m_history, filePath, &usage);
if (result.success && m_chatModel)
m_chatModel->restoreUsageFromJson(usage);
return result;
} }
void ChatHistoryStore::showSaveDialog() void ChatHistoryStore::showSaveDialog()

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -8,6 +9,10 @@
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel; class ChatModel;
@@ -17,7 +22,8 @@ class ChatHistoryStore : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr); explicit ChatHistoryStore(
ConversationHistory *history, ChatModel *chatModel, QObject *parent = nullptr);
QString historyDir() const; QString historyDir() const;
QString suggestedFileName() const; QString suggestedFileName() const;
@@ -41,6 +47,7 @@ signals:
private: private:
QString generateChatFileName(const QString &shortMessage, const QString &dir) const; QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
ConversationHistory *m_history;
ChatModel *m_chatModel; ChatModel *m_chatModel;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -7,11 +8,16 @@
#include "MessagePart.hpp" #include "MessagePart.hpp"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QHash>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QPointer>
#include <QVector>
#include <QtQmlIntegration> #include <QtQmlIntegration>
#include "context/ContentFile.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -20,15 +26,15 @@ class ChatModel : public QAbstractListModel
Q_OBJECT Q_OBJECT
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL) int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking }; enum ChatRole : int { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { enum Roles : int {
RoleType = Qt::UserRole, RoleType = Qt::UserRole,
Content, Content,
Attachments, Attachments,
@@ -42,81 +48,19 @@ public:
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
struct ImageAttachment
{
QString fileName; // Original filename
QString storedPath; // Path to stored image file (relative to chat folder)
QString mediaType; // MIME type
};
struct Message
{
ChatRole role;
QString content;
QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments;
QList<ImageAttachment> images;
QString toolName;
QJsonObject toolArguments;
QString toolResult;
int promptTokens = 0;
int completionTokens = 0;
int cachedPromptTokens = 0;
int reasoningTokens = 0;
};
explicit ChatModel(QObject *parent = nullptr); explicit ChatModel(QObject *parent = nullptr);
void setHistory(ConversationHistory *history);
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {},
bool isRedacted = false,
const QString &signature = QString());
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index); Q_INVOKABLE void resetModelTo(int index);
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const; Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
void addToolExecutionStatus(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments);
void dropTrailingAssistantMessage(const QString &requestId);
void setToolMessageData(
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments,
const QString &toolResult);
void updateToolResult(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &result);
void addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature);
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent);
void setMessageUsage( void setMessageUsage(
const QString &messageId, const QString &messageId,
int promptTokens, int promptTokens,
@@ -124,14 +68,13 @@ public:
int cachedPromptTokens, int cachedPromptTokens,
int reasoningTokens); int reasoningTokens);
QJsonObject usageToJson() const;
void restoreUsageFromJson(const QJsonObject &usage);
int sessionPromptTokens() const; int sessionPromptTokens() const;
int sessionCompletionTokens() const; int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const; int sessionCachedPromptTokens() const;
int sessionTotalTokens() const;
void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
@@ -140,18 +83,64 @@ signals:
void sessionUsageChanged(); void sessionUsageChanged();
private slots: private slots:
void onFileEditApplied(const QString &editId); void onHistoryMessageAdded(int index);
void onFileEditRejected(const QString &editId); void onHistoryMessageUpdated(int index);
void onFileEditArchived(const QString &editId); void onHistoryCleared();
void onHistoryReset();
void onFileEditStatusChanged(const QString &editId);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage); struct AttachmentRef
{
QVector<Message> m_messages; QString fileName;
bool m_loadingFromHistory = false; 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; QString m_chatFilePath;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart) Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

View File

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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -10,18 +11,23 @@
#include "ChatFileManager.hpp" #include "ChatFileManager.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
namespace QodeAssist {
class AgentFactory;
class SessionManager;
class ConversationHistory;
class Session;
} // namespace QodeAssist
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor; class ChatCompressor;
class AgentRoleController; class ChatAgentController;
class ChatConfigurationController;
class FileEditController; class FileEditController;
class InputTokenCounter; class InputTokenCounter;
class ChatHistoryStore; class ChatHistoryStore;
@@ -31,7 +37,6 @@ class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL) Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL) Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL) Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
@@ -45,24 +50,21 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL) Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL) Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL) Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL) Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged 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(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL) Q_PROPERTY(
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL) QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL) currentChatAgentChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool canCompress READ canCompress NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL) Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL) Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
@@ -73,13 +75,13 @@ public:
~ChatRootView() override; ~ChatRootView() override;
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const;
void saveHistory(const QString &filePath); void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath); void loadHistory(const QString &filePath);
Q_INVOKABLE void showSaveDialog(); Q_INVOKABLE void showSaveDialog();
Q_INVOKABLE void showLoadDialog(); Q_INVOKABLE void showLoadDialog();
Q_INVOKABLE void resetChatTo(int index);
void autosave(); void autosave();
QString getAutosaveFilePath() const; QString getAutosaveFilePath() const;
@@ -98,9 +100,10 @@ public:
Q_INVOKABLE void showAddImageDialog(); Q_INVOKABLE void showAddImageDialog();
Q_INVOKABLE bool isImageFile(const QString &filePath) const; Q_INVOKABLE bool isImageFile(const QString &filePath) const;
Q_INVOKABLE void calculateMessageTokensCount(const QString &message); 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 setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder(); Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings(); Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath); Q_INVOKABLE void openFileInEditor(const QString &filePath);
@@ -135,18 +138,10 @@ public:
void setRequestProgressStatus(bool state); void setRequestProgressStatus(bool state);
QString lastErrorMessage() const; QString lastErrorMessage() const;
QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
Q_INVOKABLE QVariantList searchSkills(const QString &query) const; Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
bool useTools() const; bool useTools() const;
void setUseTools(bool enabled);
bool useThinking() const;
void setUseThinking(bool enabled);
Q_INVOKABLE void applyFileEdit(const QString &editId); Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId); Q_INVOKABLE void rejectFileEdit(const QString &editId);
@@ -155,25 +150,15 @@ public:
Q_INVOKABLE void applyAllFileEditsForCurrentMessage(); Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats();
Q_INVOKABLE void loadAvailableConfigurations();
Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
Q_INVOKABLE void compressCurrentChat(); Q_INVOKABLE void compressCurrentChat();
Q_INVOKABLE void cancelCompression(); Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles(); Q_INVOKABLE void loadAvailableChatAgents();
Q_INVOKABLE void applyAgentRole(const QString &roleId); QStringList availableChatAgents() const;
Q_INVOKABLE void openAgentRolesSettings(); QString currentChatAgent() const;
QStringList availableAgentRoles() const; void setCurrentChatAgent(const QString &name);
QString currentAgentRole() const;
QString baseSystemPrompt() const;
QString currentAgentRoleDescription() const;
QString currentAgentRoleSystemPrompt() const;
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const; int currentMessagePendingEdits() const;
@@ -181,9 +166,8 @@ public:
QString lastInfoMessage() const; QString lastInfoMessage() const;
bool isThinkingSupport() const;
bool isCompressing() const; bool isCompressing() const;
bool canCompress() const;
bool isInEditor() const; bool isInEditor() const;
void setInEditor(bool value); void setInEditor(bool value);
@@ -202,7 +186,6 @@ public slots:
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged(); void attachmentFilesChanged();
void linkedFilesChanged(); void linkedFilesChanged();
void inputTokensCountChanged(); void inputTokensCountChanged();
@@ -218,20 +201,13 @@ signals:
void lastErrorMessageChanged(); void lastErrorMessageChanged();
void lastInfoMessageChanged(); void lastInfoMessageChanged();
void activeRulesChanged(); void sendShortcutTextChanged();
void activeRulesCountChanged();
void useToolsChanged(); void useToolsChanged();
void useThinkingChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged(); void availableChatAgentsChanged();
void availableConfigurationsChanged(); void currentChatAgentChanged();
void currentConfigurationChanged();
void availableAgentRolesChanged();
void currentAgentRoleChanged();
void baseSystemPromptChanged();
void isCompressingChanged(); void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
@@ -249,27 +225,22 @@ private:
void triggerOpenChatCommand(Utils::Id commandId); void triggerOpenChatCommand(Utils::Id commandId);
void handOffSession(); void handOffSession();
bool deferSendForAutoCompress( bool deferSendForAutoCompress(
const QString &message, const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
void dispatchSend( void dispatchSend(
const QString &message, const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
const QStringList &attachments, QString configuredCompressionAgent() const;
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
bool hasImageAttachments(const QStringList &attachments) const; bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const; SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const; Skills::SkillsManager *skillsManager() const;
AgentFactory *agentFactory() const;
SessionManager *sessionManager() const;
QodeAssist::Session *ownerSession();
QodeAssist::ConversationHistory *m_history;
ChatModel *m_chatModel; ChatModel *m_chatModel;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager; ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath; QString m_recentFilePath;
QStringList m_attachmentFiles; QStringList m_attachmentFiles;
QStringList m_linkedFiles; QStringList m_linkedFiles;
@@ -278,8 +249,6 @@ private:
QString message; QString message;
QStringList attachments; QStringList attachments;
QStringList linkedFiles; QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false; bool active = false;
}; };
PendingSend m_pendingSend; PendingSend m_pendingSend;
@@ -289,13 +258,11 @@ private:
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_lastInfoMessage; QString m_lastInfoMessage;
ChatCompressor *m_chatCompressor; ChatCompressor *m_chatCompressor;
AgentRoleController *m_agentRoleController; ChatAgentController *m_agentController;
ChatConfigurationController *m_configurationController;
FileEditController *m_fileEditController; FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter; InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore; ChatHistoryStore *m_historyStore;
@@ -303,6 +270,8 @@ private:
mutable bool m_sessionFileRegistryResolved = false; mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager; mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false; mutable bool m_skillsManagerResolved = false;
mutable QPointer<AgentFactory> m_agentFactory;
mutable QPointer<SessionManager> m_sessionManager;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

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

View File

@@ -1,13 +1,17 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QJsonArray> #include <QDateTime>
#include <QHash>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include "ChatModel.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -17,29 +21,41 @@ struct SerializationResult
QString errorMessage; QString errorMessage;
}; };
struct StoredContentEntry
{
QDateTime modified;
qint64 size = 0;
QString base64;
};
using StoredContentCache = QHash<QString, StoredContentEntry>;
class ChatSerializer class ChatSerializer
{ {
public: public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath); static const QString VERSION;
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
// Public for testing purposes static SerializationResult saveToFile(
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath); const ConversationHistory *history, const QString &filePath, const QJsonObject &usage = {});
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath); static SerializationResult loadFromFile(
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath); ConversationHistory *history, const QString &filePath, QJsonObject *usageOut = nullptr);
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
// Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath); static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage(const QString &chatFilePath, static bool saveContentToStorage(
const QString &fileName, const QString &chatFilePath,
const QString &base64Data, const QString &fileName,
QString &storedPath); const QString &base64Data,
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath); QString &storedPath);
static QString loadContentFromStorage(
const QString &chatFilePath, const QString &storedPath, StoredContentCache *cache = nullptr);
private: private:
static const QString VERSION; static QJsonObject serializeChat(const ConversationHistory *history, const QJsonObject &usage);
static constexpr int CURRENT_VERSION = 1; static SerializationResult loadCurrent(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
static SerializationResult loadLegacy(
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
static void registerHistoricalFileEdits(const ConversationHistory *history);
static bool ensureDirectoryExists(const QString &filePath); static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version); static bool validateVersion(const QString &version);

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatUtils.h" #include "ChatUtils.h"

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatView.hpp" #include "ChatView.hpp"

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatWidget.hpp" #include "ChatWidget.hpp"

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,29 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QSet> #include <QPointer>
#include <QString> #include <QString>
#include <QVector> #include <QStringList>
#include <memory>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Provider.hpp" #include "ChatSerializer.hpp"
#include "pluginllmcore/IPromptProvider.hpp" #include <ErrorInfo.hpp>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <ResponseEvent.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
} // namespace QodeAssist
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
@@ -25,77 +35,70 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager); void setSkillsManager(Skills::SkillsManager *skillsManager);
void setSessionManager(SessionManager *sessionManager);
void setHistory(ConversationHistory *history);
void setActiveAgent(const QString &agentName);
void sendMessage( void sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments = {}, const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {}, const QList<QString> &linkedFiles = {});
bool useTools = false,
bool useThinking = false);
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
Context::ContextManager *contextManager() const; Context::ContextManager *contextManager() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
void ensureSession();
Session *session() const;
signals: signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely(); void messageReceivedCompletely();
void requestStarted(const QString &requestId); void requestStarted(const QString &requestId);
void requestCancelled();
void messageUsageReceived( void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens); int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
private slots:
void handlePartialResponse(const QString &requestId, const QString &partialText);
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
void handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void handleToolExecutionStarted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments);
void handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput);
private: private:
void handleLLMResponse(const QString &response, const QJsonObject &request); bool ensureAgentBound();
QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles( void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
const QString &basePrompt, const QList<QString> &linkedFiles) const; void onSessionFinished(const QString &requestId);
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
void onSessionCancelled(const QString &requestId);
QStringList invokedSkillNames(const QString &message) const;
QString buildChatContextLayer() const;
QString requestIdForSession(Session *session) const;
bool isImageFile(const QString &filePath) const; bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
bool dropPreToolText = false;
}; };
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QPointer<ConversationHistory> m_history;
Skills::SkillsManager *m_skillsManager = nullptr; Skills::SkillsManager *m_skillsManager = nullptr;
QPointer<SessionManager> m_sessionManager;
QPointer<Session> m_session;
QString m_activeAgent;
QString m_boundAgent;
QString m_chatFilePath; QString m_chatFilePath;
std::shared_ptr<StoredContentCache> m_contentCache;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
QSet<QString> m_awaitingContinuation;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FileEditController.hpp" #include "FileEditController.hpp"
@@ -9,15 +10,13 @@
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include "ChatModel.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "context/ChangesManager.h" #include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent) FileEditController::FileEditController(QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel)
{ {
auto &changes = Context::ChangesManager::instance(); auto &changes = Context::ChangesManager::instance();
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) { connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
@@ -79,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId)); LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) { if (Context::ChangesManager::instance().applyFileEdit(editId)) {
emit infoMessage(QString("File edit applied successfully")); emit infoMessage(QString("File edit applied successfully"));
updateFileEditStatus(editId, "applied");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -94,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId)); LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) { if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
emit infoMessage(QString("File edit rejected")); emit infoMessage(QString("File edit rejected"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -109,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId)); LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) { if (Context::ChangesManager::instance().undoFileEdit(editId)) {
emit infoMessage(QString("File edit undone successfully")); emit infoMessage(QString("File edit undone successfully"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -162,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath)); LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
} }
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
{
auto messages = m_chatModel->getChatHistory();
for (int i = 0; i < messages.size(); ++i) {
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
QString content = messages[i].content;
const QString marker = "QODEASSIST_FILE_EDIT:";
int markerPos = content.indexOf(marker);
QString jsonStr = content;
if (markerPos >= 0) {
jsonStr = content.mid(markerPos + marker.length());
}
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject obj = doc.object();
obj["status"] = status;
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (!edit.statusMessage.isEmpty()) {
obj["status_message"] = edit.statusMessage;
}
QString updatedContent = marker
+ QString::fromUtf8(
QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_chatModel->updateMessageContent(editId, updatedContent);
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
}
break;
}
}
updateStats();
}
void FileEditController::applyAllForCurrentMessage() void FileEditController::applyAllForCurrentMessage()
{ {
if (m_currentRequestId.isEmpty()) { if (m_currentRequestId.isEmpty()) {
@@ -222,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
: QString("Failed to apply some file edits:\n%1").arg(errorMsg)); : QString("Failed to apply some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
updateStats(); updateStats();
} }
@@ -254,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
: QString("Failed to undo some file edits:\n%1").arg(errorMsg)); : QString("Failed to undo some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
updateStats(); updateStats();
} }

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -8,14 +9,12 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class FileEditController : public QObject class FileEditController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr); explicit FileEditController(QObject *parent = nullptr);
void setCurrentRequestId(const QString &requestId); void setCurrentRequestId(const QString &requestId);
void clearCurrentRequestId(); void clearCurrentRequestId();
@@ -40,9 +39,6 @@ signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status);
ChatModel *m_chatModel;
QString m_currentRequestId; QString m_currentRequestId;
int m_totalEdits{0}; int m_totalEdits{0};
int m_appliedEdits{0}; int m_appliedEdits{0};

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FileItem.hpp" #include "FileItem.hpp"

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,31 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QStringList> #include <QStringList>
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Context { namespace QodeAssist::Context {
class ContextManager; class ContextManager;
} }
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class InputTokenCounter : public QObject class InputTokenCounter : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
InputTokenCounter( InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr); ConversationHistory *history,
Context::ContextManager *contextManager,
QObject *parent = nullptr);
int inputTokens() const; int inputTokens() const;
@@ -29,25 +34,22 @@ public:
void setLinkedFiles(const QStringList &linkedFiles); void setLinkedFiles(const QStringList &linkedFiles);
void recompute(); void recompute();
void recordSent(); void recordServerUsage(int promptTokens, int cachedTokens);
void recordServerUsage(int promptTokens); void resetServerUsage();
signals: signals:
void inputTokensChanged(); void inputTokensChanged();
private: private:
void rewireToolsChangedConnection(); ConversationHistory *m_history;
ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QMetaObject::Connection m_toolsChangedConn;
QStringList m_attachments; QStringList m_attachments;
QStringList m_linkedFiles; QStringList m_linkedFiles;
int m_messageTokens{0}; int m_messageTokens{0};
int m_inputTokens{0}; int m_inputTokens{0};
int m_lastSentEstimate{0}; int m_serverInputTokens{0};
double m_calibrationFactor{1.0}; bool m_hasServerUsage{false};
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "SessionFileRegistry.hpp" #include "SessionFileRegistry.hpp"
@@ -7,29 +8,43 @@
#include <QFileInfo> #include <QFileInfo>
#include <Session.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
SessionFileRegistry::SessionFileRegistry(QObject *parent) SessionFileRegistry::SessionFileRegistry(QObject *parent)
: QObject(parent) : QObject(parent)
{} {}
SessionFileRegistry::~SessionFileRegistry() = default;
bool SessionFileRegistry::isLocked(const QString &path) const bool SessionFileRegistry::isLocked(const QString &path) const
{ {
return !path.isEmpty() && m_lockedPaths.contains(path); return !path.isEmpty() && !m_locks.value(path).isNull();
} }
bool SessionFileRegistry::lock(const QString &path) bool SessionFileRegistry::isLockedByOther(const QString &path, QodeAssist::Session *self) const
{ {
if (path.isEmpty() || m_lockedPaths.contains(path)) { if (path.isEmpty())
return false; return false;
} const auto owner = m_locks.value(path);
m_lockedPaths.insert(path); return !owner.isNull() && owner != self;
}
bool SessionFileRegistry::lock(const QString &path, QodeAssist::Session *owner)
{
if (path.isEmpty())
return false;
const auto existing = m_locks.value(path);
if (!existing.isNull() && existing != owner)
return false;
m_locks.insert(path, owner);
return true; return true;
} }
void SessionFileRegistry::release(const QString &path) void SessionFileRegistry::release(const QString &path)
{ {
m_lockedPaths.remove(path); m_locks.remove(path);
} }
void SessionFileRegistry::setPendingChatFile(const QString &path) void SessionFileRegistry::setPendingChatFile(const QString &path)
@@ -44,7 +59,7 @@ QString SessionFileRegistry::takePendingChatFile()
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
{ {
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) { if (desiredPath.isEmpty() || !isLocked(desiredPath)) {
return desiredPath; return desiredPath;
} }
@@ -58,7 +73,7 @@ QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
if (!suffix.isEmpty()) { if (!suffix.isEmpty()) {
candidate += '.' + suffix; candidate += '.' + suffix;
} }
if (!m_lockedPaths.contains(candidate)) { if (!isLocked(candidate)) {
return candidate; return candidate;
} }
} }

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 233 B

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -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 // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -19,6 +20,9 @@ ChatRootView {
colorGroup: SystemPalette.Active colorGroup: SystemPalette.Active
} }
property bool hasActiveError: false
readonly property color errorColor: "#d32f2f"
palette { palette {
window: sysPalette.window window: sysPalette.window
windowText: sysPalette.windowText windowText: sysPalette.windowText
@@ -114,7 +118,6 @@ ChatRootView {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved") text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
} }
openChatHistory.onClicked: root.openChatHistoryFolder() openChatHistory.onClicked: root.openChatHistoryFolder()
contextButton.onClicked: contextViewer.open()
pinButton { pinButton {
visible: typeof _chatview !== 'undefined' visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
@@ -134,43 +137,18 @@ ChatRootView {
relocateTooltip.text: (typeof _chatview !== 'undefined') relocateTooltip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor tab") ? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window") : qsTr("Move this chat to a separate window")
toolsButton {
checked: root.useTools
onCheckedChanged: {
root.useTools = toolsButton.checked
}
}
thinkingMode {
checked: root.useThinking
enabled: root.isThinkingSupport
onCheckedChanged: {
root.useThinking = thinkingMode.checked
}
}
settingsButton.onClicked: root.openSettings() settingsButton.onClicked: root.openSettings()
configSelector { agentSelector {
model: root.availableConfigurations model: root.availableChatAgents
displayText: root.currentConfiguration displayText: root.currentChatAgent
onActivated: function(index) { onActivated: function(index) {
if (index > 0) { root.currentChatAgent = root.availableChatAgents[index]
root.applyConfiguration(root.availableConfigurations[index])
}
} }
Component.onCompleted: root.loadAvailableChatAgents()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableConfigurations() root.loadAvailableChatAgents()
}
}
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
} }
} }
} }
@@ -339,7 +317,7 @@ ChatRootView {
onResetChatToMessage: function(idx) { onResetChatToMessage: function(idx) {
messageInput.text = model.content messageInput.text = model.content
messageInput.cursorPosition = model.content.length messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx) root.resetChatTo(idx)
} }
onOpenFileRequested: function(filePath) { onOpenFileRequested: function(filePath) {
@@ -411,11 +389,10 @@ ChatRootView {
QQC.TextArea { QQC.TextArea {
id: messageInput id: messageInput
placeholderText: Qt.platform.os === "osx" placeholderText: qsTr("Type your message here... (%1 to send)").arg(root.sendShortcutText)
? qsTr("Type your message here... (⌘+↩ to send)")
: qsTr("Type your message here... (Ctrl+Enter to send)")
placeholderTextColor: palette.mid placeholderTextColor: palette.mid
color: palette.text color: palette.text
wrapMode: TextArea.Wrap
background: Rectangle { background: Rectangle {
radius: 2 radius: 2
color: palette.base color: palette.base
@@ -494,6 +471,9 @@ ChatRootView {
skillCommandPopup.dismiss() skillCommandPopup.dismiss()
event.accepted = true event.accepted = true
} }
} else if (root.isSendShortcut(event.key, event.modifiers)) {
root.sendChatMessage()
event.accepted = true
} }
} }
@@ -586,13 +566,25 @@ ChatRootView {
Layout.preferredHeight: 40 Layout.preferredHeight: 40
isCompressing: root.isCompressing isCompressing: root.isCompressing
isProcessing: root.isRequestInProgress
canCompress: root.canCompress
canSend: root.currentChatAgent !== ""
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage() sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest() : root.cancelRequest()
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg" sendButton.icon.source: root.isRequestInProgress
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg" ? ""
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop") : (root.hasActiveError ? "qrc:/qt/qml/ChatView/icons/warning-icon.svg"
sendButtonTooltip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return") : "qrc:/qt/qml/ChatView/icons/chat-icon.svg")
: qsTr("Stop") 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() compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression() cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles { syncOpenFiles {
@@ -667,6 +659,11 @@ ChatRootView {
} }
function sendChatMessage() { function sendChatMessage() {
if (root.isCompressing)
return
if (messageInput.text.trim() === "" && root.attachmentFiles.length === 0)
return
root.hasActiveError = false
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text)) root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = "" messageInput.text = ""
fileMentionPopup.clearMentions() fileMentionPopup.clearMentions()
@@ -689,13 +686,122 @@ ChatRootView {
onAccepted: root.compressCurrentChat() onAccepted: root.compressCurrentChat()
} }
Toast { Rectangle {
id: errorToast id: errorBanner
z: 1000
color: Qt.rgba(0.8, 0.2, 0.2, 0.9) z: 1000
border.color: Qt.darker(infoToast.color, 1.3) visible: root.hasActiveError && root.lastErrorMessage.length > 0
toastTextColor: "#FFFFFF"
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 { Toast {
@@ -707,35 +813,11 @@ ChatRootView {
toastTextColor: "#FFFFFF" toastTextColor: "#FFFFFF"
} }
ContextViewer {
id: contextViewer
width: Math.min(parent.width * 0.85, 800)
height: Math.min(parent.height * 0.85, 700)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
baseSystemPrompt: root.baseSystemPrompt
currentAgentRole: root.currentAgentRole
currentAgentRoleDescription: root.currentAgentRoleDescription
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
activeRules: root.activeRules
activeRulesCount: root.activeRulesCount
onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
}
Connections { Connections {
target: root target: root
function onLastErrorMessageChanged() { function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) { if (root.lastErrorMessage.length > 0) {
errorToast.show(root.lastErrorMessage) root.hasActiveError = true
} }
} }
function onLastInfoMessageChanged() { function onLastInfoMessageChanged() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -19,6 +20,9 @@ Rectangle {
property alias cancelCompressButton: cancelCompressButtonId property alias cancelCompressButton: cancelCompressButtonId
property bool isCompressing: false property bool isCompressing: false
property bool isProcessing: false
property bool canCompress: true
property bool canSend: true
property alias sendButtonTooltip: sendButtonTooltipId property alias sendButtonTooltip: sendButtonTooltipId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
@@ -107,7 +111,7 @@ Rectangle {
visible: root.isCompressing visible: root.isCompressing
spacing: 6 spacing: 6
BusyIndicator { QoABusyIndicator {
id: compressBusyIndicator id: compressBusyIndicator
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -137,37 +141,78 @@ Rectangle {
} }
} }
QoAButton { Item {
id: compressButtonId id: compressButtonContainer
visible: !root.isCompressing visible: !root.isCompressing
text: qsTr("Compress") implicitWidth: compressButtonId.implicitWidth
implicitHeight: compressButtonId.implicitHeight
icon { QoAButton {
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg" id: compressButtonId
height: 15
width: 15 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 { QoAToolTip {
visible: compressButtonId.hovered visible: compressHoverHandler.hovered
delay: 250 delay: 250
text: qsTr("Compress chat (create summarized copy using LLM)") text: root.canCompress
? qsTr("Compress chat (create summarized copy using LLM)")
: qsTr("Assign a compression agent in the Pipelines settings")
} }
} }
QoAButton { Item {
id: sendButtonId id: sendButtonContainer
icon { implicitWidth: sendButtonId.implicitWidth
height: 15 implicitHeight: sendButtonId.implicitHeight
width: 15
QoAButton {
id: sendButtonId
anchors.fill: parent
enabled: root.isProcessing || (root.canSend && !root.isCompressing)
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 { QoAToolTip {
id: sendButtonTooltipId id: sendButtonTooltipId
visible: sendButtonId.hovered visible: sendHoverHandler.hovered
delay: 250 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 // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt> // Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
@@ -208,24 +209,4 @@ QString CodeHandler::detectLanguageFromExtension(const QString &extension)
return extensionToLanguage.value(extension.toLower(), ""); return extensionToLanguage.value(extension.toLower(), "");
} }
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
{
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -31,10 +32,6 @@ public:
private: private:
static QString getCommentPrefix(const QString &language); static QString getCommentPrefix(const QString &language);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex();
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,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 GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
{ {
"Id" : "qodeassist", "Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.9.19", "Version" : "0.9.21",
"CompatVersion" : "${IDE_VERSION}", "CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev", "Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev", "VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", "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).", "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", "Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "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 * You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/ */
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
@@ -157,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this); m_refactorWidgetHandler = new RefactorWidgetHandler(this);
} }
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
QodeAssistClient::~QodeAssistClient() QodeAssistClient::~QodeAssistClient()
{ {
cleanupConnections(); cleanupConnections();
@@ -260,10 +272,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager()->shouldIgnore(
if (m_llmClient->contextManager() editor->textDocument()->filePath().toUrlishString())) {
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -295,6 +305,12 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
QTC_ASSERT(editor, return); QTC_ASSERT(editor, return);
handleCompletions(response, editor); handleCompletions(response, editor);
}); });
connect(
editor,
&TextEditorWidget::destroyed,
this,
&QodeAssistClient::onEditorDestroyed,
Qt::UniqueConnection);
m_runningRequests[editor] = request; m_runningRequests[editor] = request;
sendMessage(request); sendMessage(request);
} }
@@ -307,9 +323,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager() if (m_llmClient->contextManager()->shouldIgnore(
->ignoreManager() editor->textDocument()->filePath().toUrlishString())) {
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -317,6 +332,8 @@ void QodeAssistClient::requestQuickRefactor(
if (!m_refactorHandler) { if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this); m_refactorHandler = new QuickRefactorHandler(this);
m_refactorHandler->setSessionManager(m_sessionManager);
m_refactorHandler->setAgentFactory(m_agentFactory);
connect( connect(
m_refactorHandler, m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted, &QuickRefactorHandler::refactoringCompleted,
@@ -355,10 +372,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
return; return;
requestCompletions(editor); requestCompletions(editor);
}); });
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() { connect(
delete m_scheduledRequests.take(editor); editor,
cancelRunningRequest(editor); &TextEditorWidget::destroyed,
}); this,
&QodeAssistClient::onEditorDestroyed,
Qt::UniqueConnection);
it = m_scheduledRequests.insert(editor, timer); it = m_scheduledRequests.insert(editor, timer);
} }
@@ -374,9 +393,12 @@ void QodeAssistClient::handleCompletions(
editor->abortAssist(); editor->abortAssist();
if (response.error()) { if (response.error()) {
m_runningRequests.remove(editor);
log(*response.error()); log(*response.error());
m_errorHandler if (response.error()->code() != -32800) {
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message())); m_errorHandler
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
}
return; return;
} }
@@ -477,6 +499,13 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
m_runningRequests.erase(it); m_runningRequests.erase(it);
} }
void QodeAssistClient::onEditorDestroyed(QObject *editorObject)
{
auto *editor = static_cast<TextEditor::TextEditorWidget *>(editorObject);
delete m_scheduledRequests.take(editor);
cancelRunningRequest(editor);
}
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
{ {
if (!project) if (!project)
@@ -518,6 +547,11 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{ {
m_progressHandler.hideProgress(); m_progressHandler.hideProgress();
if (result.cancelled) {
LOG_MESSAGE("Refactoring request was cancelled");
return;
}
if (!result.success) { if (!result.success) {
QString errorMessage = result.errorMessage.isEmpty() QString errorMessage = result.errorMessage.isEmpty()
? tr("Quick refactor failed") ? tr("Quick refactor failed")
@@ -536,6 +570,15 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
return; return;
} }
if (result.documentRevision >= 0
&& result.editor->document()->revision() != result.documentRevision) {
m_errorHandler.showError(
result.editor,
tr("Quick refactor discarded: the document changed while the request was running"));
LOG_MESSAGE("Refactoring result discarded: document revision changed");
return;
}
int displayMode = Settings::quickRefactorSettings().displayMode(); int displayMode = Settings::quickRefactorSettings().displayMode();
if (displayMode == 0) { if (displayMode == 0) {
@@ -632,7 +675,14 @@ void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
displayRefactored = result.newText; displayRefactored = result.newText;
} }
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) { const int revisionAtDisplay = editorWidget->document()->revision();
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result, revisionAtDisplay](
const QString &editedText) {
if (editorWidget->document()->revision() != revisionAtDisplay) {
m_errorHandler.showError(
editorWidget, tr("Quick refactor discarded: the document changed before applying"));
return;
}
applyRefactoringEdit(editorWidget, result.insertRange, editedText); applyRefactoringEdit(editorWidget, result.insertRange, editedText);
}); });

View File

@@ -1,25 +1,27 @@
// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QPointer>
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp" #include "LSPCompletion.hpp"
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
#include "RefactorSuggestionHoverHandler.hpp" #include "RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp" #include "widgets/CompletionErrorHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp" #include "widgets/CompletionProgressHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp" #include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class AgentFactory;
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
Q_OBJECT Q_OBJECT
@@ -27,6 +29,9 @@ public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override; ~QodeAssistClient() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override; bool canOpenProject(ProjectExplorer::Project *project) override;
@@ -42,6 +47,7 @@ private:
void handleCompletions( void handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor); const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor); void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
void onEditorDestroyed(QObject *editorObject);
bool isEnabled(ProjectExplorer::Project *project) const; bool isEnabled(ProjectExplorer::Project *project) const;
void setupConnections(); void setupConnections();
@@ -62,11 +68,12 @@ private:
int m_recentCharCount; int m_recentCharCount;
CompletionProgressHandler m_progressHandler; CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler; CompletionErrorHandler m_errorHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr}; QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr}; RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient; LLMClientInterface *m_llmClient;
SessionManager *m_sessionManager{nullptr};
AgentFactory *m_agentFactory{nullptr};
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

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

View File

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

191
README.md
View File

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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "RefactorSuggestion.hpp" #include "RefactorSuggestion.hpp"
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "RefactorSuggestionHoverHandler.hpp" #include "RefactorSuggestionHoverHandler.hpp"
#include "RefactorSuggestion.hpp" #include "RefactorSuggestion.hpp"

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -31,7 +32,6 @@ public:
void setSuggestionRange(const Utils::Text::Range &range); void setSuggestionRange(const Utils::Text::Range &range);
void clearSuggestionRange(); void clearSuggestionRange();
bool hasSuggestion() const { return m_hasSuggestion; }
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); } void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); } void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }

View File

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

View File

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

View File

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