mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-29 17:49:12 -04:00
Compare commits
19 Commits
v0.9.19
...
refactor-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d66c714a28 | ||
|
|
86c537477d | ||
|
|
70c6d30a72 | ||
|
|
747dfb540e | ||
|
|
e200278f9a | ||
|
|
755263c4de | ||
|
|
a6921f523a | ||
|
|
dc3100f054 | ||
|
|
ccc2ec2e80 | ||
|
|
34ce787320 | ||
|
|
52ef8318dc | ||
|
|
449317ab91 | ||
|
|
abb3351246 | ||
|
|
57eeb32ceb | ||
|
|
74eed49fb4 | ||
|
|
43a30281b6 | ||
|
|
bf4307c459 | ||
|
|
6df70e608b | ||
|
|
ee1bf4ffe5 |
24
.github/workflows/build_cmake.yml
vendored
24
.github/workflows/build_cmake.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
config:
|
||||
- {
|
||||
name: "Windows Latest MSVC", artifact: "Windows-x64",
|
||||
os: windows-latest,
|
||||
os: windows-2022,
|
||||
platform: windows_x64,
|
||||
cc: "cl", cxx: "cl",
|
||||
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
|
||||
@@ -53,6 +53,10 @@ jobs:
|
||||
qt_version: "6.10.3",
|
||||
qt_creator_version: "19.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.11.1",
|
||||
qt_creator_version: "20.0.0"
|
||||
}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
||||
@@ -110,10 +114,14 @@ jobs:
|
||||
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
|
||||
|
||||
string(REPLACE "." "" qt_version_dotless "${qt_version}")
|
||||
set(qt_repo_dir "qt6_${qt_version_dotless}")
|
||||
if ("${{ runner.os }}" STREQUAL "Windows")
|
||||
set(url_os "windows_x86")
|
||||
set(qt_package_arch_suffix "win64_msvc2022_64")
|
||||
set(qt_dir_prefix "${qt_version}/msvc2022_64")
|
||||
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
|
||||
set(qt_repo_dir "qt6_${qt_version_dotless}_msvc2022_64")
|
||||
endif()
|
||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
|
||||
else()
|
||||
@@ -127,7 +135,9 @@ jobs:
|
||||
set(qt_package_arch_suffix "linux_gcc_64")
|
||||
endif()
|
||||
set(qt_dir_prefix "${qt_version}/gcc_64")
|
||||
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
|
||||
set(qt_package_suffix "-Linux-RHEL_9_6-GCC-Linux-RHEL_9_6-X86_64")
|
||||
elseif (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
|
||||
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
|
||||
else()
|
||||
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
|
||||
@@ -143,7 +153,7 @@ jobs:
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
|
||||
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/${qt_repo_dir}")
|
||||
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
|
||||
|
||||
file(READ ./Updates.xml updates_xml)
|
||||
@@ -170,7 +180,11 @@ jobs:
|
||||
)
|
||||
endforeach()
|
||||
|
||||
foreach(package qt5compat qtshadertools)
|
||||
set(qt_addon_packages qt5compat qtshadertools)
|
||||
if (qt_version VERSION_GREATER_EQUAL "6.11.0")
|
||||
list(APPEND qt_addon_packages qttasktree)
|
||||
endif()
|
||||
foreach(package ${qt_addon_packages})
|
||||
downloadAndExtract(
|
||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||
${package}.7z
|
||||
@@ -236,7 +250,7 @@ jobs:
|
||||
endif()
|
||||
|
||||
set(build_plugin_py "scripts/build_plugin.py")
|
||||
foreach(dir "share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts")
|
||||
foreach(dir "share/qtcreator/scripts" "Qt Creator.sdk/share/qtcreator/scripts" "Qt Creator.app/Contents/Resources/scripts" "Contents/Resources/scripts")
|
||||
if(EXISTS "${{ steps.qt_creator.outputs.qtc_dir }}/${dir}/build_plugin.py")
|
||||
set(build_plugin_py "${dir}/build_plugin.py")
|
||||
break()
|
||||
|
||||
@@ -2,10 +2,6 @@ cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(QodeAssist)
|
||||
|
||||
option(QODEASSIST_EXPERIMENTAL
|
||||
"Enable experimental features" OFF)
|
||||
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
@@ -42,7 +38,6 @@ add_definitions(
|
||||
|
||||
add_subdirectory(sources)
|
||||
add_subdirectory(logger)
|
||||
add_subdirectory(pluginllmcore)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(UIControls)
|
||||
add_subdirectory(ChatView)
|
||||
@@ -69,7 +64,6 @@ add_qtc_plugin(QodeAssist
|
||||
QtCreator::Utils
|
||||
QtCreator::CPlusPlus
|
||||
LLMQore
|
||||
PluginLLMCore
|
||||
ProvidersConfig
|
||||
Agents
|
||||
Skills
|
||||
@@ -83,42 +77,6 @@ add_qtc_plugin(QodeAssist
|
||||
QodeAssisttr.h
|
||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||
RefactorContextHelper.hpp
|
||||
templates/Templates.hpp
|
||||
templates/CodeLlamaFim.hpp
|
||||
templates/Ollama.hpp
|
||||
templates/Claude.hpp
|
||||
templates/OpenAI.hpp
|
||||
templates/MistralAI.hpp
|
||||
templates/StarCoder2Fim.hpp
|
||||
templates/Qwen25CoderFIM.hpp
|
||||
templates/OpenAICompatible.hpp
|
||||
templates/Llama3.hpp
|
||||
templates/ChatML.hpp
|
||||
templates/Alpaca.hpp
|
||||
templates/Llama2.hpp
|
||||
templates/CodeLlamaQMLFim.hpp
|
||||
templates/GoogleAI.hpp
|
||||
templates/LlamaCppFim.hpp
|
||||
templates/Qwen3CoderFIM.hpp
|
||||
templates/OpenAIResponses.hpp
|
||||
providers/Providers.hpp
|
||||
providers/ProviderUrlUtils.hpp
|
||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
||||
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
|
||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
||||
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
||||
providers/QwenProvider.hpp providers/QwenProvider.cpp
|
||||
providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp
|
||||
providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp
|
||||
QodeAssist.qrc
|
||||
LSPCompletion.hpp
|
||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||
@@ -130,17 +88,12 @@ add_qtc_plugin(QodeAssist
|
||||
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
||||
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
||||
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
CodeHandler.hpp CodeHandler.cpp
|
||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
||||
widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp
|
||||
widgets/CompletionHintWidget.hpp widgets/CompletionHintWidget.cpp
|
||||
widgets/CompletionHintHandler.hpp widgets/CompletionHintHandler.cpp
|
||||
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
|
||||
widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp
|
||||
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
|
||||
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
|
||||
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
||||
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
||||
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
||||
@@ -170,10 +123,7 @@ add_qtc_plugin(QodeAssist
|
||||
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||
)
|
||||
|
||||
if(QODEASSIST_EXPERIMENTAL)
|
||||
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
|
||||
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
|
||||
endif()
|
||||
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
|
||||
|
||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||
find_program(QtCreatorExecutable
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -22,7 +22,6 @@ qt_add_qml_module(QodeAssistChatView
|
||||
qml/controls/BottomBar.qml
|
||||
qml/controls/FileMentionPopup.qml
|
||||
qml/controls/FileEditsActionBar.qml
|
||||
qml/controls/ContextViewer.qml
|
||||
qml/controls/SkillCommandPopup.qml
|
||||
qml/controls/Toast.qml
|
||||
qml/controls/TopBar.qml
|
||||
@@ -45,9 +44,8 @@ qt_add_qml_module(QodeAssistChatView
|
||||
icons/window-unlock.svg
|
||||
icons/chat-icon.svg
|
||||
icons/chat-pause-icon.svg
|
||||
icons/warning-icon.svg
|
||||
icons/new-chat-icon.svg
|
||||
icons/rules-icon.svg
|
||||
icons/context-icon.svg
|
||||
icons/open-in-editor.svg
|
||||
icons/open-in-window.svg
|
||||
icons/apply-changes-button.svg
|
||||
@@ -74,8 +72,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
FileItem.hpp FileItem.cpp
|
||||
ChatFileManager.hpp ChatFileManager.cpp
|
||||
ChatCompressor.hpp ChatCompressor.cpp
|
||||
AgentRoleController.hpp AgentRoleController.cpp
|
||||
ChatConfigurationController.hpp ChatConfigurationController.cpp
|
||||
ChatAgentController.hpp ChatAgentController.cpp
|
||||
FileEditController.hpp FileEditController.cpp
|
||||
InputTokenCounter.hpp InputTokenCounter.cpp
|
||||
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
||||
@@ -91,13 +88,14 @@ target_link_libraries(QodeAssistChatView
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
PluginLLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
QodeAssistUIControlsplugin
|
||||
QodeAssistLogger
|
||||
LLMQore
|
||||
Skills
|
||||
Agents
|
||||
Session
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
|
||||
108
ChatView/ChatAgentController.cpp
Normal file
108
ChatView/ChatAgentController.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatAgentController.hpp"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
#include <AgentFactory.hpp>
|
||||
#include <sources/settings/PipelinesConfig.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
namespace {
|
||||
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
|
||||
}
|
||||
|
||||
ChatAgentController::ChatAgentController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
m_currentAgent = settings->value(kChatAgentKey).toString();
|
||||
}
|
||||
|
||||
void ChatAgentController::setAgentFactory(AgentFactory *factory)
|
||||
{
|
||||
m_agentFactory = factory;
|
||||
if (factory)
|
||||
connect(
|
||||
factory,
|
||||
&AgentFactory::agentsChanged,
|
||||
this,
|
||||
&ChatAgentController::reload,
|
||||
Qt::UniqueConnection);
|
||||
reload();
|
||||
}
|
||||
|
||||
QStringList ChatAgentController::availableAgents() const
|
||||
{
|
||||
return m_availableAgents;
|
||||
}
|
||||
|
||||
QString ChatAgentController::currentAgent() const
|
||||
{
|
||||
return m_currentAgent;
|
||||
}
|
||||
|
||||
void ChatAgentController::setCurrentAgent(const QString &name)
|
||||
{
|
||||
if (name == m_currentAgent || !m_availableAgents.contains(name))
|
||||
return;
|
||||
|
||||
applyCurrentAgent(name);
|
||||
}
|
||||
|
||||
void ChatAgentController::reload()
|
||||
{
|
||||
if (!m_agentFactory)
|
||||
return;
|
||||
|
||||
const QStringList all = m_agentFactory->configNames();
|
||||
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatAssistant;
|
||||
|
||||
QStringList filtered;
|
||||
for (const QString &name : roster) {
|
||||
if (all.contains(name))
|
||||
filtered.append(name);
|
||||
}
|
||||
|
||||
if (filtered != m_availableAgents) {
|
||||
m_availableAgents = filtered;
|
||||
emit availableAgentsChanged();
|
||||
}
|
||||
ensureValidCurrent();
|
||||
}
|
||||
|
||||
void ChatAgentController::ensureValidCurrent()
|
||||
{
|
||||
if (m_availableAgents.contains(m_currentAgent))
|
||||
return;
|
||||
|
||||
const QString next = m_availableAgents.isEmpty() ? QString() : m_availableAgents.first();
|
||||
if (next == m_currentAgent)
|
||||
return;
|
||||
|
||||
applyCurrentAgent(next);
|
||||
}
|
||||
|
||||
void ChatAgentController::applyCurrentAgent(const QString &name)
|
||||
{
|
||||
m_currentAgent = name;
|
||||
if (auto *settings = Core::ICore::settings())
|
||||
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||
emit currentAgentChanged();
|
||||
}
|
||||
|
||||
bool ChatAgentController::currentSupportsTools() const
|
||||
{
|
||||
if (!m_agentFactory || m_currentAgent.isEmpty())
|
||||
return false;
|
||||
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
|
||||
return config && config->enableTools;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
48
ChatView/ChatAgentController.hpp
Normal file
48
ChatView/ChatAgentController.hpp
Normal 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
|
||||
@@ -1,21 +1,34 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatCompressor.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include "ChatModel.hpp"
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
|
||||
#include <AgentFactory.hpp>
|
||||
#include <ContextRenderer.hpp>
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QStringList>
|
||||
#include <QUuid>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
@@ -24,7 +37,18 @@ ChatCompressor::ChatCompressor(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
||||
void ChatCompressor::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void ChatCompressor::setActiveAgent(const QString &agentName)
|
||||
{
|
||||
m_activeAgent = agentName;
|
||||
}
|
||||
|
||||
void ChatCompressor::startCompression(
|
||||
const QString &chatFilePath, ConversationHistory *sourceHistory)
|
||||
{
|
||||
if (m_isCompressing) {
|
||||
emit compressionFailed(tr("Compression already in progress"));
|
||||
@@ -36,47 +60,84 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chatModel || chatModel->rowCount() == 0) {
|
||||
if (!sourceHistory || sourceHistory->isEmpty()) {
|
||||
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!m_provider) {
|
||||
emit compressionFailed(tr("No provider available"));
|
||||
if (!m_sessionManager) {
|
||||
emit compressionFailed(tr("Chat session manager is not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||
templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
emit compressionFailed(tr("No template available"));
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
|
||||
if (!session) {
|
||||
emit compressionFailed(
|
||||
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
|
||||
return;
|
||||
}
|
||||
|
||||
auto *client = session->client();
|
||||
if (!client) {
|
||||
m_sessionManager->removeSession(session);
|
||||
emit compressionFailed(tr("Chat agent has no live client"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
Templates::ContextRenderer::Bindings bindings;
|
||||
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
||||
bindings.configDir = AgentFactory::userConfigDir();
|
||||
session->setContextBindings(bindings);
|
||||
|
||||
m_isCompressing = true;
|
||||
m_chatModel = chatModel;
|
||||
m_originalChatPath = chatFilePath;
|
||||
m_accumulatedSummary.clear();
|
||||
m_session = session;
|
||||
|
||||
emit compressionStarted();
|
||||
|
||||
connectProviderSignals();
|
||||
QStringList transcriptParts;
|
||||
for (const auto &msg : sourceHistory->messages()) {
|
||||
if (msg.role() != Message::Role::User && msg.role() != Message::Role::Assistant)
|
||||
continue;
|
||||
const QString text = msg.text();
|
||||
if (text.trimmed().isEmpty())
|
||||
continue;
|
||||
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
const QString role = msg.role() == Message::Role::User ? QStringLiteral("User")
|
||||
: QStringLiteral("Assistant");
|
||||
transcriptParts.append(QStringLiteral("%1: %2").arg(role, text));
|
||||
}
|
||||
|
||||
buildRequestPayload(payload, promptTemplate);
|
||||
if (transcriptParts.isEmpty()) {
|
||||
handleCompressionError(tr("Chat is empty, nothing to compress"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
m_currentRequestId = m_provider->sendRequest(
|
||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
const QString transcript = transcriptParts.join(QStringLiteral("\n\n"));
|
||||
|
||||
connect(
|
||||
session, &Session::finished, this,
|
||||
[this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); });
|
||||
connect(
|
||||
session, &Session::failed, this,
|
||||
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||
onCompressionFailed(id, error.message);
|
||||
});
|
||||
|
||||
client->setTransferTimeout(
|
||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(transcript));
|
||||
|
||||
m_currentRequestId = session->send(std::move(blocks));
|
||||
if (m_currentRequestId.isEmpty()) {
|
||||
handleCompressionError(tr("Failed to start compression request: %1")
|
||||
.arg(session->lastError().message));
|
||||
return;
|
||||
}
|
||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||
}
|
||||
|
||||
@@ -91,44 +152,38 @@ void ChatCompressor::cancelCompression()
|
||||
return;
|
||||
|
||||
LOG_MESSAGE("Cancelling compression request");
|
||||
|
||||
if (m_provider && !m_currentRequestId.isEmpty())
|
||||
m_provider->cancelRequest(m_currentRequestId);
|
||||
|
||||
cleanupState();
|
||||
emit compressionFailed(tr("Compression cancelled"));
|
||||
}
|
||||
|
||||
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
||||
void ChatCompressor::onCompressionFinished(const QString &requestId)
|
||||
{
|
||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||
return;
|
||||
|
||||
m_accumulatedSummary += partialText;
|
||||
}
|
||||
QString summary;
|
||||
if (m_session) {
|
||||
if (auto *history = m_session->history(); history && !history->isEmpty())
|
||||
summary = history->messages().back().text();
|
||||
}
|
||||
|
||||
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
||||
{
|
||||
Q_UNUSED(fullText)
|
||||
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
|
||||
|
||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||
return;
|
||||
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||
const QString sourcePath = m_originalChatPath;
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
||||
cleanupState();
|
||||
|
||||
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
||||
handleCompressionError(tr("Failed to save compressed chat"));
|
||||
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
|
||||
emit compressionFailed(tr("Failed to save compressed chat"));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||
cleanupState();
|
||||
emit compressionCompleted(compressedPath);
|
||||
}
|
||||
|
||||
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
||||
void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||
return;
|
||||
@@ -151,53 +206,6 @@ QString ChatCompressor::createCompressedChatPath(const QString &originalPath) co
|
||||
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
|
||||
}
|
||||
|
||||
QString ChatCompressor::buildCompressionPrompt() const
|
||||
{
|
||||
return QStringLiteral(
|
||||
"Please create a comprehensive summary of our entire conversation above. "
|
||||
"The summary should:\n"
|
||||
"1. Preserve all important context, decisions, and key information\n"
|
||||
"2. Maintain technical details, code snippets, file references, and specific examples\n"
|
||||
"3. Keep the chronological flow of the discussion\n"
|
||||
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
|
||||
"5. Be written in clear, structured format\n"
|
||||
"6. Use markdown formatting for better readability\n\n"
|
||||
"Create the summary now:");
|
||||
}
|
||||
|
||||
void ChatCompressor::buildRequestPayload(
|
||||
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
||||
{
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
context.systemPrompt = QStringLiteral(
|
||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool
|
||||
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||
continue;
|
||||
|
||||
PluginLLMCore::Message apiMessage;
|
||||
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
PluginLLMCore::Message compressionRequest;
|
||||
compressionRequest.role = "user";
|
||||
compressionRequest.content = buildCompressionPrompt();
|
||||
messages.append(compressionRequest);
|
||||
|
||||
context.history = messages;
|
||||
|
||||
m_provider->prepareRequest(
|
||||
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
||||
}
|
||||
|
||||
bool ChatCompressor::createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||
{
|
||||
@@ -221,11 +229,11 @@ bool ChatCompressor::createCompressedChatFile(
|
||||
|
||||
QJsonObject summaryMessage;
|
||||
summaryMessage["role"] = "assistant";
|
||||
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
summaryMessage["isRedacted"] = false;
|
||||
summaryMessage["attachments"] = QJsonArray();
|
||||
summaryMessage["images"] = QJsonArray();
|
||||
QJsonObject textBlock;
|
||||
textBlock["type"] = "text";
|
||||
textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||
summaryMessage["blocks"] = QJsonArray{textBlock};
|
||||
|
||||
root["messages"] = QJsonArray{summaryMessage};
|
||||
root["compressedFrom"] = sourcePath;
|
||||
@@ -244,49 +252,17 @@ bool ChatCompressor::createCompressedChatFile(
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatCompressor::connectProviderSignals()
|
||||
{
|
||||
auto *c = m_provider->client();
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ChatCompressor::onPartialResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&ChatCompressor::onFullResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ChatCompressor::onRequestFailed,
|
||||
Qt::UniqueConnection));
|
||||
}
|
||||
|
||||
void ChatCompressor::disconnectAllSignals()
|
||||
{
|
||||
for (const auto &connection : std::as_const(m_connections))
|
||||
disconnect(connection);
|
||||
m_connections.clear();
|
||||
}
|
||||
|
||||
void ChatCompressor::cleanupState()
|
||||
{
|
||||
disconnectAllSignals();
|
||||
Session *session = m_session;
|
||||
|
||||
m_isCompressing = false;
|
||||
m_currentRequestId.clear();
|
||||
m_originalChatPath.clear();
|
||||
m_accumulatedSummary.clear();
|
||||
m_chatModel = nullptr;
|
||||
m_provider = nullptr;
|
||||
m_session = nullptr;
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->release(session);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::PluginLLMCore {
|
||||
class Provider;
|
||||
class PromptTemplate;
|
||||
} // namespace QodeAssist::PluginLLMCore
|
||||
namespace QodeAssist {
|
||||
class SessionManager;
|
||||
class Session;
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel;
|
||||
|
||||
class ChatCompressor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -24,7 +24,10 @@ class ChatCompressor : public QObject
|
||||
public:
|
||||
explicit ChatCompressor(QObject *parent = nullptr);
|
||||
|
||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setActiveAgent(const QString &agentName);
|
||||
|
||||
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
|
||||
|
||||
bool isCompressing() const;
|
||||
void cancelCompression();
|
||||
@@ -34,30 +37,22 @@ signals:
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
void compressionFailed(const QString &error);
|
||||
|
||||
private slots:
|
||||
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
||||
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
||||
void onRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
void onCompressionFinished(const QString &requestId);
|
||||
void onCompressionFailed(const QString &requestId, const QString &error);
|
||||
|
||||
QString createCompressedChatPath(const QString &originalPath) const;
|
||||
QString buildCompressionPrompt() const;
|
||||
bool createCompressedChatFile(
|
||||
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||
void connectProviderSignals();
|
||||
void disconnectAllSignals();
|
||||
void cleanupState();
|
||||
void handleCompressionError(const QString &error);
|
||||
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
||||
|
||||
bool m_isCompressing = false;
|
||||
QString m_currentRequestId;
|
||||
QString m_originalChatPath;
|
||||
QString m_accumulatedSummary;
|
||||
PluginLLMCore::Provider *m_provider = nullptr;
|
||||
ChatModel *m_chatModel = nullptr;
|
||||
|
||||
QList<QMetaObject::Connection> m_connections;
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QString m_activeAgent;
|
||||
QPointer<Session> m_session;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatHistoryStore.hpp"
|
||||
|
||||
@@ -15,15 +16,20 @@
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <PluginBlocks.hpp>
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
|
||||
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
, m_history(history)
|
||||
{}
|
||||
|
||||
QString ChatHistoryStore::historyDir() const
|
||||
@@ -51,17 +57,23 @@ QString ChatHistoryStore::suggestedFileName() const
|
||||
{
|
||||
QString shortMessage;
|
||||
|
||||
if (m_chatModel->rowCount() > 0) {
|
||||
QString firstMessage
|
||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
||||
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||
if (m_history) {
|
||||
for (const auto &message : m_history->messages()) {
|
||||
if (message.role() != Message::Role::User)
|
||||
continue;
|
||||
|
||||
if (shortMessage.isEmpty()) {
|
||||
QVariantList images
|
||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
||||
if (!images.isEmpty()) {
|
||||
shortMessage = "image_chat";
|
||||
const QString text = message.text();
|
||||
if (!text.trimmed().isEmpty()) {
|
||||
shortMessage = text.split('\n').first().simplified().left(30);
|
||||
} else {
|
||||
for (const auto &block : message.blocks()) {
|
||||
if (dynamic_cast<StoredImageContent *>(block.get())) {
|
||||
shortMessage = "image_chat";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +118,12 @@ QString ChatHistoryStore::autosaveFilePath(
|
||||
|
||||
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
||||
{
|
||||
return ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||
return ChatSerializer::saveToFile(m_history, filePath);
|
||||
}
|
||||
|
||||
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
||||
{
|
||||
return ChatSerializer::loadFromFile(m_chatModel, filePath);
|
||||
return ChatSerializer::loadFromFile(m_history, filePath);
|
||||
}
|
||||
|
||||
void ChatHistoryStore::showSaveDialog()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -8,16 +9,18 @@
|
||||
|
||||
#include "ChatSerializer.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
namespace QodeAssist {
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
class ChatModel;
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatHistoryStore : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
|
||||
|
||||
QString historyDir() const;
|
||||
QString suggestedFileName() const;
|
||||
@@ -41,7 +44,7 @@ signals:
|
||||
private:
|
||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
ConversationHistory *m_history;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -7,11 +8,16 @@
|
||||
#include "MessagePart.hpp"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QPointer>
|
||||
#include <QVector>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "context/ContentFile.hpp"
|
||||
namespace QodeAssist {
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@@ -21,14 +27,13 @@ class ChatModel : public QAbstractListModel
|
||||
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
|
||||
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||
enum ChatRole : int { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
enum Roles {
|
||||
enum Roles : int {
|
||||
RoleType = Qt::UserRole,
|
||||
Content,
|
||||
Attachments,
|
||||
@@ -42,81 +47,19 @@ public:
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
struct ImageAttachment
|
||||
{
|
||||
QString fileName; // Original filename
|
||||
QString storedPath; // Path to stored image file (relative to chat folder)
|
||||
QString mediaType; // MIME type
|
||||
};
|
||||
|
||||
struct Message
|
||||
{
|
||||
ChatRole role;
|
||||
QString content;
|
||||
QString id;
|
||||
bool isRedacted = false;
|
||||
QString signature = QString();
|
||||
|
||||
QList<Context::ContentFile> attachments;
|
||||
QList<ImageAttachment> images;
|
||||
|
||||
QString toolName;
|
||||
QJsonObject toolArguments;
|
||||
QString toolResult;
|
||||
|
||||
int promptTokens = 0;
|
||||
int completionTokens = 0;
|
||||
int cachedPromptTokens = 0;
|
||||
int reasoningTokens = 0;
|
||||
};
|
||||
|
||||
explicit ChatModel(QObject *parent = nullptr);
|
||||
|
||||
void setHistory(ConversationHistory *history);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void addMessage(
|
||||
const QString &content,
|
||||
ChatRole role,
|
||||
const QString &id,
|
||||
const QList<Context::ContentFile> &attachments = {},
|
||||
const QList<ImageAttachment> &images = {},
|
||||
bool isRedacted = false,
|
||||
const QString &signature = QString());
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||
|
||||
QVector<Message> getChatHistory() const;
|
||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
||||
|
||||
QString currentModel() const;
|
||||
QString lastMessageId() const;
|
||||
|
||||
Q_INVOKABLE void resetModelTo(int index);
|
||||
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
||||
|
||||
void addToolExecutionStatus(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &toolArguments);
|
||||
void dropTrailingAssistantMessage(const QString &requestId);
|
||||
void setToolMessageData(
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &toolArguments,
|
||||
const QString &toolResult);
|
||||
void updateToolResult(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &result);
|
||||
void addThinkingBlock(
|
||||
const QString &requestId, const QString &thinking, const QString &signature);
|
||||
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
||||
|
||||
void setMessageUsage(
|
||||
const QString &messageId,
|
||||
int promptTokens,
|
||||
@@ -127,11 +70,7 @@ public:
|
||||
int sessionPromptTokens() const;
|
||||
int sessionCompletionTokens() const;
|
||||
int sessionCachedPromptTokens() const;
|
||||
int sessionTotalTokens() const;
|
||||
|
||||
void setLoadingFromHistory(bool loading);
|
||||
bool isLoadingFromHistory() const;
|
||||
|
||||
|
||||
void setChatFilePath(const QString &filePath);
|
||||
QString chatFilePath() const;
|
||||
|
||||
@@ -140,18 +79,64 @@ signals:
|
||||
void sessionUsageChanged();
|
||||
|
||||
private slots:
|
||||
void onFileEditApplied(const QString &editId);
|
||||
void onFileEditRejected(const QString &editId);
|
||||
void onFileEditArchived(const QString &editId);
|
||||
void onHistoryMessageAdded(int index);
|
||||
void onHistoryMessageUpdated(int index);
|
||||
void onHistoryCleared();
|
||||
void onHistoryReset();
|
||||
void onFileEditStatusChanged(const QString &editId);
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
||||
|
||||
QVector<Message> m_messages;
|
||||
bool m_loadingFromHistory = false;
|
||||
struct AttachmentRef
|
||||
{
|
||||
QString fileName;
|
||||
QString storedPath;
|
||||
};
|
||||
struct ImageRef
|
||||
{
|
||||
QString fileName;
|
||||
QString storedPath;
|
||||
QString mediaType;
|
||||
};
|
||||
struct Row
|
||||
{
|
||||
ChatRole kind = ChatRole::Assistant;
|
||||
int messageIndex = -1;
|
||||
QString messageId;
|
||||
QString content;
|
||||
bool isRedacted = false;
|
||||
QString editId;
|
||||
QString fileEditDisplay;
|
||||
QVector<AttachmentRef> attachments;
|
||||
QVector<ImageRef> images;
|
||||
};
|
||||
struct Usage
|
||||
{
|
||||
int prompt = 0;
|
||||
int completion = 0;
|
||||
int cached = 0;
|
||||
int reasoning = 0;
|
||||
};
|
||||
|
||||
void rebuildAll();
|
||||
void reprojectTail(int startMessageIndex);
|
||||
int startMessageIndexFor(int messageIndex) const;
|
||||
int firstRowForMessage(int messageIndex) const;
|
||||
QHash<QString, QString> buildToolResultMap() const;
|
||||
void mergeToolResultsFromMessage(int messageIndex);
|
||||
void pruneUsageToHistory();
|
||||
void appendRowsForMessage(
|
||||
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
|
||||
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
|
||||
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
|
||||
QVariantList buildImageList(const QVector<ImageRef> &images) const;
|
||||
|
||||
QPointer<ConversationHistory> m_history;
|
||||
QVector<Row> m_rows;
|
||||
QHash<QString, QString> m_toolResults;
|
||||
QHash<QString, Usage> m_usageByMessageId;
|
||||
QString m_chatFilePath;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
|
||||
|
||||
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatRootView.hpp"
|
||||
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QKeySequence>
|
||||
#include <QMessageBox>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
@@ -26,9 +28,14 @@
|
||||
|
||||
#include "QodeAssistConstants.hpp"
|
||||
|
||||
#include "AgentRoleController.hpp"
|
||||
#include <AgentFactory.hpp>
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <sources/settings/PipelinesConfig.hpp>
|
||||
|
||||
#include "ChatAgentController.hpp"
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatConfigurationController.hpp"
|
||||
#include "ChatCompressor.hpp"
|
||||
#include "ChatHistoryStore.hpp"
|
||||
#include "FileEditController.hpp"
|
||||
@@ -36,10 +43,8 @@
|
||||
#include "InputTokenCounter.hpp"
|
||||
#include "SettingsConstants.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "SessionFileRegistry.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "pluginllmcore/RulesLoader.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "SkillsSettings.hpp"
|
||||
#include "sources/skills/SkillsManager.hpp"
|
||||
@@ -52,23 +57,39 @@ bool isChatEditor(Core::IEditor *editor)
|
||||
return editor && editor->document()
|
||||
&& editor->document()->id() == Utils::Id(Constants::QODE_ASSIST_CHAT_EDITOR_ID);
|
||||
}
|
||||
|
||||
QKeySequence sendMessageKeySequence()
|
||||
{
|
||||
auto command = Core::ActionManager::command(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE);
|
||||
if (!command)
|
||||
return {};
|
||||
|
||||
QKeySequence sequence = command->keySequence();
|
||||
if (sequence.isEmpty()) {
|
||||
const QList<QKeySequence> defaults = command->defaultKeySequences();
|
||||
if (!defaults.isEmpty())
|
||||
sequence = defaults.constFirst();
|
||||
}
|
||||
return sequence;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
: QQuickItem(parent)
|
||||
, m_history(new QodeAssist::ConversationHistory(this))
|
||||
, m_chatModel(new ChatModel(this))
|
||||
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||
, m_fileManager(new ChatFileManager(this))
|
||||
, m_isRequestInProgress(false)
|
||||
, m_chatCompressor(new ChatCompressor(this))
|
||||
, m_agentRoleController(new AgentRoleController(this))
|
||||
, m_configurationController(new ChatConfigurationController(this))
|
||||
, m_fileEditController(new FileEditController(m_chatModel, this))
|
||||
, m_tokenCounter(
|
||||
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
|
||||
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
|
||||
, m_agentController(new ChatAgentController(this))
|
||||
, m_fileEditController(new FileEditController(this))
|
||||
, m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
|
||||
, m_historyStore(new ChatHistoryStore(m_history, this))
|
||||
{
|
||||
m_chatModel->setHistory(m_history);
|
||||
m_clientInterface->setHistory(m_history);
|
||||
|
||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().linkOpenFiles,
|
||||
@@ -76,21 +97,21 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); });
|
||||
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
connect(
|
||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
||||
|
||||
connect(
|
||||
m_configurationController,
|
||||
&ChatConfigurationController::availableConfigurationsChanged,
|
||||
QMetaObject::invokeMethod(
|
||||
this,
|
||||
&ChatRootView::availableConfigurationsChanged);
|
||||
connect(
|
||||
m_configurationController,
|
||||
&ChatConfigurationController::currentConfigurationChanged,
|
||||
this,
|
||||
&ChatRootView::currentConfigurationChanged);
|
||||
[this] {
|
||||
if (auto sendCommand
|
||||
= Core::ActionManager::command(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE)) {
|
||||
connect(
|
||||
sendCommand,
|
||||
&Core::Command::keySequenceChanged,
|
||||
this,
|
||||
&ChatRootView::sendShortcutTextChanged,
|
||||
Qt::UniqueConnection);
|
||||
}
|
||||
emit sendShortcutTextChanged();
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(
|
||||
m_clientInterface,
|
||||
@@ -110,6 +131,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
|
||||
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
||||
setRecentFilePath(QString{});
|
||||
m_tokenCounter->resetServerUsage();
|
||||
m_fileEditController->clearCurrentRequestId();
|
||||
});
|
||||
auto maybeEmitTitle = [this] {
|
||||
@@ -138,20 +160,20 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::inputTokensCountChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::availableRolesChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::availableAgentsChanged,
|
||||
this,
|
||||
&ChatRootView::availableAgentRolesChanged);
|
||||
&ChatRootView::availableChatAgentsChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::currentRoleChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::currentAgentChanged,
|
||||
this,
|
||||
&ChatRootView::currentAgentRoleChanged);
|
||||
&ChatRootView::currentChatAgentChanged);
|
||||
connect(
|
||||
m_agentRoleController,
|
||||
&AgentRoleController::baseSystemPromptChanged,
|
||||
m_agentController,
|
||||
&ChatAgentController::currentAgentChanged,
|
||||
this,
|
||||
&ChatRootView::baseSystemPromptChanged);
|
||||
&ChatRootView::useToolsChanged);
|
||||
|
||||
auto editors = Core::EditorManager::instance();
|
||||
|
||||
@@ -210,8 +232,8 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
m_clientInterface,
|
||||
&ClientInterface::messageUsageReceived,
|
||||
this,
|
||||
[this](int promptTokens, int /*completionTokens*/, int /*cached*/, int /*reasoning*/) {
|
||||
m_tokenCounter->recordServerUsage(promptTokens);
|
||||
[this](int promptTokens, int /*completionTokens*/, int cachedTokens, int /*reasoning*/) {
|
||||
m_tokenCounter->recordServerUsage(promptTokens, cachedTokens);
|
||||
});
|
||||
|
||||
connect(
|
||||
@@ -233,14 +255,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
connect(
|
||||
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
||||
|
||||
refreshRules();
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
||||
this,
|
||||
&ChatRootView::refreshRules);
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::projectAdded,
|
||||
@@ -253,24 +267,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::openFilesChanged);
|
||||
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().enableChatTools,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::useToolsChanged);
|
||||
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().enableThinkingMode,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::useThinkingChanged);
|
||||
|
||||
connect(
|
||||
&Settings::generalSettings().caProvider,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatRootView::isThinkingSupportChanged);
|
||||
|
||||
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
||||
m_lastErrorMessage = error;
|
||||
emit lastErrorMessageChanged();
|
||||
@@ -291,7 +287,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
if (m_pendingSend.active) {
|
||||
PendingSend p = m_pendingSend;
|
||||
m_pendingSend = {};
|
||||
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
||||
dispatchSend(p.message, p.attachments, p.linkedFiles);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -304,7 +300,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
if (m_pendingSend.active) {
|
||||
PendingSend p = m_pendingSend;
|
||||
m_pendingSend = {};
|
||||
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
||||
dispatchSend(p.message, p.attachments, p.linkedFiles);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -340,6 +336,57 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
|
||||
return m_skillsManager;
|
||||
}
|
||||
|
||||
AgentFactory *ChatRootView::agentFactory() const
|
||||
{
|
||||
if (!m_agentFactory) {
|
||||
if (auto *engine = qmlEngine(this)) {
|
||||
m_agentFactory = qobject_cast<AgentFactory *>(
|
||||
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
|
||||
}
|
||||
}
|
||||
return m_agentFactory;
|
||||
}
|
||||
|
||||
SessionManager *ChatRootView::sessionManager() const
|
||||
{
|
||||
if (!m_sessionManager) {
|
||||
if (auto *engine = qmlEngine(this)) {
|
||||
m_sessionManager = qobject_cast<SessionManager *>(
|
||||
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
|
||||
}
|
||||
}
|
||||
return m_sessionManager;
|
||||
}
|
||||
|
||||
QodeAssist::Session *ChatRootView::ownerSession()
|
||||
{
|
||||
if (!m_clientInterface)
|
||||
return nullptr;
|
||||
m_clientInterface->setSessionManager(sessionManager());
|
||||
m_clientInterface->ensureSession();
|
||||
return m_clientInterface->session();
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableChatAgents()
|
||||
{
|
||||
m_agentController->setAgentFactory(agentFactory());
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableChatAgents() const
|
||||
{
|
||||
return m_agentController->availableAgents();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentChatAgent() const
|
||||
{
|
||||
return m_agentController->currentAgent();
|
||||
}
|
||||
|
||||
void ChatRootView::setCurrentChatAgent(const QString &name)
|
||||
{
|
||||
m_agentController->setCurrentAgent(name);
|
||||
}
|
||||
|
||||
QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||
{
|
||||
QVariantList results;
|
||||
@@ -347,7 +394,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||
if (!manager || !Settings::skillsSettings().enableSkills())
|
||||
return results;
|
||||
|
||||
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
@@ -383,21 +430,17 @@ void ChatRootView::sendMessage(const QString &message)
|
||||
{
|
||||
const QStringList attachments = m_attachmentFiles;
|
||||
const QStringList linkedFiles = m_linkedFiles;
|
||||
const bool tools = useTools();
|
||||
const bool thinking = useThinking();
|
||||
|
||||
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking))
|
||||
if (deferSendForAutoCompress(message, attachments, linkedFiles))
|
||||
return;
|
||||
|
||||
dispatchSend(message, attachments, linkedFiles, tools, thinking);
|
||||
dispatchSend(message, attachments, linkedFiles);
|
||||
}
|
||||
|
||||
bool ChatRootView::deferSendForAutoCompress(
|
||||
const QString &message,
|
||||
const QStringList &attachments,
|
||||
const QStringList &linkedFiles,
|
||||
bool useToolsArg,
|
||||
bool useThinkingArg)
|
||||
const QStringList &linkedFiles)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
if (!settings.autoCompress())
|
||||
@@ -408,6 +451,9 @@ bool ChatRootView::deferSendForAutoCompress(
|
||||
if (inputTokens < threshold)
|
||||
return false;
|
||||
|
||||
if (configuredCompressionAgent().isEmpty())
|
||||
return false;
|
||||
|
||||
if (m_recentFilePath.isEmpty()) {
|
||||
QString filePath = getAutosaveFilePath(message, attachments);
|
||||
if (filePath.isEmpty())
|
||||
@@ -423,7 +469,7 @@ bool ChatRootView::deferSendForAutoCompress(
|
||||
.arg(inputTokens)
|
||||
.arg(threshold));
|
||||
|
||||
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
|
||||
m_pendingSend = {message, attachments, linkedFiles, true};
|
||||
compressCurrentChat();
|
||||
return true;
|
||||
}
|
||||
@@ -431,9 +477,7 @@ bool ChatRootView::deferSendForAutoCompress(
|
||||
void ChatRootView::dispatchSend(
|
||||
const QString &message,
|
||||
const QStringList &attachments,
|
||||
const QStringList &linkedFiles,
|
||||
bool useToolsArg,
|
||||
bool useThinkingArg)
|
||||
const QStringList &linkedFiles)
|
||||
{
|
||||
if (m_recentFilePath.isEmpty()) {
|
||||
QString filePath = getAutosaveFilePath(message, attachments);
|
||||
@@ -446,10 +490,13 @@ void ChatRootView::dispatchSend(
|
||||
}
|
||||
}
|
||||
|
||||
m_tokenCounter->recordSent();
|
||||
if (currentChatAgent().isEmpty())
|
||||
loadAvailableChatAgents();
|
||||
|
||||
m_clientInterface->setSkillsManager(skillsManager());
|
||||
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
||||
m_clientInterface->setSessionManager(sessionManager());
|
||||
m_clientInterface->setActiveAgent(currentChatAgent());
|
||||
m_clientInterface->sendMessage(message, attachments, linkedFiles);
|
||||
|
||||
m_fileManager->clearIntermediateStorage();
|
||||
clearAttachmentFiles();
|
||||
@@ -494,21 +541,14 @@ void ChatRootView::clearMessages()
|
||||
clearLinkedFiles();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentTemplate() const
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
return settings.caModel();
|
||||
}
|
||||
|
||||
void ChatRootView::saveHistory(const QString &filePath)
|
||||
{
|
||||
if (filePath != m_recentFilePath) {
|
||||
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
|
||||
m_lastErrorMessage
|
||||
= tr("This chat file is already in use by another QodeAssist chat session.");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
if (auto registry = sessionFileRegistry();
|
||||
registry && registry->isLockedByOther(filePath, ownerSession())) {
|
||||
m_lastErrorMessage = tr(
|
||||
"This chat file is already in use by another QodeAssist chat session.");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = m_historyStore->save(filePath);
|
||||
@@ -521,13 +561,11 @@ void ChatRootView::saveHistory(const QString &filePath)
|
||||
|
||||
void ChatRootView::loadHistory(const QString &filePath)
|
||||
{
|
||||
if (filePath != m_recentFilePath) {
|
||||
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
|
||||
m_lastErrorMessage
|
||||
= tr("This chat is already open in another QodeAssist chat session.");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
if (auto registry = sessionFileRegistry();
|
||||
registry && registry->isLockedByOther(filePath, ownerSession())) {
|
||||
m_lastErrorMessage = tr("This chat is already open in another QodeAssist chat session.");
|
||||
emit lastErrorMessageChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = m_historyStore->load(filePath);
|
||||
@@ -743,6 +781,32 @@ void ChatRootView::calculateMessageTokensCount(const QString &message)
|
||||
m_tokenCounter->setMessage(message);
|
||||
}
|
||||
|
||||
bool ChatRootView::isSendShortcut(int key, int modifiers) const
|
||||
{
|
||||
const QKeySequence sequence = sendMessageKeySequence();
|
||||
if (sequence.isEmpty())
|
||||
return false;
|
||||
|
||||
const QKeyCombination combination = sequence[0];
|
||||
const int sequenceKey = combination.key();
|
||||
|
||||
const int relevantMask = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier
|
||||
| Qt::MetaModifier;
|
||||
const int sequenceModifiers = combination.keyboardModifiers() & relevantMask;
|
||||
const int eventModifiers = modifiers & relevantMask;
|
||||
|
||||
const bool isReturnLike = sequenceKey == Qt::Key_Return || sequenceKey == Qt::Key_Enter;
|
||||
const bool keyMatches = key == sequenceKey
|
||||
|| (isReturnLike && (key == Qt::Key_Return || key == Qt::Key_Enter));
|
||||
|
||||
return keyMatches && eventModifiers == sequenceModifiers;
|
||||
}
|
||||
|
||||
QString ChatRootView::sendShortcutText() const
|
||||
{
|
||||
return sendMessageKeySequence().toString(QKeySequence::NativeText);
|
||||
}
|
||||
|
||||
void ChatRootView::setIsSyncOpenFiles(bool state)
|
||||
{
|
||||
if (m_isSyncOpenFiles != state) {
|
||||
@@ -762,25 +826,6 @@ void ChatRootView::openChatHistoryFolder()
|
||||
m_historyStore->openHistoryFolder();
|
||||
}
|
||||
|
||||
void ChatRootView::openRulesFolder()
|
||||
{
|
||||
auto project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString projectPath = project->projectDirectory().toFSPathString();
|
||||
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
||||
|
||||
QDir dir(rulesPath);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void ChatRootView::openSettings()
|
||||
{
|
||||
QMetaObject::invokeMethod(
|
||||
@@ -831,13 +876,12 @@ QString ChatRootView::chatTitle() const
|
||||
|
||||
QString ChatRootView::computeChatTitle() const
|
||||
{
|
||||
if (!m_chatModel)
|
||||
if (!m_history)
|
||||
return {};
|
||||
const auto history = m_chatModel->getChatHistory();
|
||||
for (const auto &msg : history) {
|
||||
if (msg.role != ChatModel::User)
|
||||
for (const auto &msg : m_history->messages()) {
|
||||
if (msg.role() != Message::Role::User)
|
||||
continue;
|
||||
const QString content = msg.content.trimmed();
|
||||
const QString content = msg.text().trimmed();
|
||||
if (content.isEmpty())
|
||||
continue;
|
||||
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
|
||||
@@ -993,7 +1037,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
||||
registry->release(m_recentFilePath);
|
||||
}
|
||||
if (!filePath.isEmpty()) {
|
||||
registry->lock(filePath);
|
||||
registry->lock(filePath, ownerSession());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,11 +1049,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
||||
|
||||
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
||||
{
|
||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
||||
if (project
|
||||
&& m_clientInterface->contextManager()
|
||||
->ignoreManager()
|
||||
->shouldIgnore(filePath.toFSPathString(), project)) {
|
||||
if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
|
||||
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
||||
.arg(filePath.toFSPathString()));
|
||||
return true;
|
||||
@@ -1061,71 +1101,9 @@ QString ChatRootView::lastErrorMessage() const
|
||||
return m_lastErrorMessage;
|
||||
}
|
||||
|
||||
QVariantList ChatRootView::activeRules() const
|
||||
{
|
||||
return m_activeRules;
|
||||
}
|
||||
|
||||
int ChatRootView::activeRulesCount() const
|
||||
{
|
||||
return m_activeRules.size();
|
||||
}
|
||||
|
||||
QString ChatRootView::getRuleContent(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_activeRules.size())
|
||||
return QString();
|
||||
|
||||
return PluginLLMCore::RulesLoader::loadRuleFileContent(
|
||||
m_activeRules[index].toMap()["filePath"].toString());
|
||||
}
|
||||
|
||||
void ChatRootView::refreshRules()
|
||||
{
|
||||
m_activeRules.clear();
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (!project) {
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
auto ruleFiles
|
||||
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||
|
||||
for (const auto &ruleFile : ruleFiles) {
|
||||
QVariantMap ruleMap;
|
||||
ruleMap["filePath"] = ruleFile.filePath;
|
||||
ruleMap["fileName"] = ruleFile.fileName;
|
||||
ruleMap["category"] = ruleFile.category;
|
||||
m_activeRules.append(ruleMap);
|
||||
}
|
||||
|
||||
emit activeRulesChanged();
|
||||
emit activeRulesCountChanged();
|
||||
}
|
||||
|
||||
bool ChatRootView::useTools() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().enableChatTools();
|
||||
}
|
||||
|
||||
void ChatRootView::setUseTools(bool enabled)
|
||||
{
|
||||
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
|
||||
Settings::chatAssistantSettings().writeSettings();
|
||||
}
|
||||
|
||||
bool ChatRootView::useThinking() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().enableThinkingMode();
|
||||
}
|
||||
|
||||
void ChatRootView::setUseThinking(bool enabled)
|
||||
{
|
||||
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
|
||||
Settings::chatAssistantSettings().writeSettings();
|
||||
return m_agentController->currentSupportsTools();
|
||||
}
|
||||
|
||||
void ChatRootView::applyFileEdit(const QString &editId)
|
||||
@@ -1158,11 +1136,6 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
|
||||
m_fileEditController->undoAllForCurrentMessage();
|
||||
}
|
||||
|
||||
void ChatRootView::updateCurrentMessageEditsStats()
|
||||
{
|
||||
m_fileEditController->updateStats();
|
||||
}
|
||||
|
||||
int ChatRootView::currentMessageTotalEdits() const
|
||||
{
|
||||
return m_fileEditController->totalEdits();
|
||||
@@ -1188,14 +1161,6 @@ QString ChatRootView::lastInfoMessage() const
|
||||
return m_lastInfoMessage;
|
||||
}
|
||||
|
||||
bool ChatRootView::isThinkingSupport() const
|
||||
{
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
||||
}
|
||||
|
||||
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
||||
{
|
||||
for (const QString &filePath : attachments) {
|
||||
@@ -1214,64 +1179,17 @@ bool ChatRootView::isImageFile(const QString &filePath) const
|
||||
return imageExtensions.contains(fileInfo.suffix().toLower());
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableConfigurations()
|
||||
QString ChatRootView::configuredCompressionAgent() const
|
||||
{
|
||||
m_configurationController->loadAvailableConfigurations();
|
||||
const QString configured = Settings::PipelinesConfig::load().rosters.chatCompression;
|
||||
if (!configured.isEmpty() && agentFactory() && agentFactory()->configByName(configured))
|
||||
return configured;
|
||||
return {};
|
||||
}
|
||||
|
||||
void ChatRootView::applyConfiguration(const QString &configName)
|
||||
bool ChatRootView::canCompress() const
|
||||
{
|
||||
m_configurationController->applyConfiguration(configName);
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableConfigurations() const
|
||||
{
|
||||
return m_configurationController->availableConfigurations();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentConfiguration() const
|
||||
{
|
||||
return m_configurationController->currentConfiguration();
|
||||
}
|
||||
|
||||
void ChatRootView::loadAvailableAgentRoles()
|
||||
{
|
||||
m_agentRoleController->loadAvailableRoles();
|
||||
}
|
||||
|
||||
void ChatRootView::applyAgentRole(const QString &roleName)
|
||||
{
|
||||
m_agentRoleController->applyRole(roleName);
|
||||
}
|
||||
|
||||
QStringList ChatRootView::availableAgentRoles() const
|
||||
{
|
||||
return m_agentRoleController->availableRoles();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRole() const
|
||||
{
|
||||
return m_agentRoleController->currentRole();
|
||||
}
|
||||
|
||||
QString ChatRootView::baseSystemPrompt() const
|
||||
{
|
||||
return m_agentRoleController->baseSystemPrompt();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleDescription() const
|
||||
{
|
||||
return m_agentRoleController->currentRoleDescription();
|
||||
}
|
||||
|
||||
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
||||
{
|
||||
return m_agentRoleController->currentRoleSystemPrompt();
|
||||
}
|
||||
|
||||
void ChatRootView::openAgentRolesSettings()
|
||||
{
|
||||
m_agentRoleController->openSettings();
|
||||
return !configuredCompressionAgent().isEmpty();
|
||||
}
|
||||
|
||||
void ChatRootView::compressCurrentChat()
|
||||
@@ -1288,9 +1206,15 @@ void ChatRootView::compressCurrentChat()
|
||||
return;
|
||||
}
|
||||
|
||||
const QString compressionAgent = configuredCompressionAgent();
|
||||
if (compressionAgent.isEmpty())
|
||||
return;
|
||||
|
||||
autosave();
|
||||
|
||||
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
||||
m_chatCompressor->setSessionManager(sessionManager());
|
||||
m_chatCompressor->setActiveAgent(compressionAgent);
|
||||
m_chatCompressor->startCompression(m_recentFilePath, m_history);
|
||||
}
|
||||
|
||||
void ChatRootView::cancelCompression()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -10,18 +11,23 @@
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist {
|
||||
class AgentFactory;
|
||||
class SessionManager;
|
||||
class ConversationHistory;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatCompressor;
|
||||
class AgentRoleController;
|
||||
class ChatConfigurationController;
|
||||
class ChatAgentController;
|
||||
class FileEditController;
|
||||
class InputTokenCounter;
|
||||
class ChatHistoryStore;
|
||||
@@ -31,7 +37,6 @@ class ChatRootView : public QQuickItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||
@@ -45,24 +50,17 @@ class ChatRootView : public QQuickItem
|
||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
||||
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
||||
Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
|
||||
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
||||
|
||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
|
||||
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
|
||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||
Q_PROPERTY(bool canCompress READ canCompress NOTIFY availableChatAgentsChanged FINAL)
|
||||
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
||||
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
||||
|
||||
@@ -73,7 +71,6 @@ public:
|
||||
~ChatRootView() override;
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
|
||||
void saveHistory(const QString &filePath);
|
||||
void loadHistory(const QString &filePath);
|
||||
@@ -98,9 +95,10 @@ public:
|
||||
Q_INVOKABLE void showAddImageDialog();
|
||||
Q_INVOKABLE bool isImageFile(const QString &filePath) const;
|
||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||
Q_INVOKABLE bool isSendShortcut(int key, int modifiers) const;
|
||||
QString sendShortcutText() const;
|
||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||
Q_INVOKABLE void openChatHistoryFolder();
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
Q_INVOKABLE void openSettings();
|
||||
|
||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||
@@ -135,18 +133,10 @@ public:
|
||||
void setRequestProgressStatus(bool state);
|
||||
|
||||
QString lastErrorMessage() const;
|
||||
|
||||
QVariantList activeRules() const;
|
||||
int activeRulesCount() const;
|
||||
Q_INVOKABLE QString getRuleContent(int index);
|
||||
Q_INVOKABLE void refreshRules();
|
||||
|
||||
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
||||
|
||||
bool useTools() const;
|
||||
void setUseTools(bool enabled);
|
||||
bool useThinking() const;
|
||||
void setUseThinking(bool enabled);
|
||||
|
||||
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
||||
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
||||
@@ -155,25 +145,15 @@ public:
|
||||
|
||||
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
|
||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
||||
|
||||
Q_INVOKABLE void loadAvailableConfigurations();
|
||||
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
||||
QStringList availableConfigurations() const;
|
||||
QString currentConfiguration() const;
|
||||
|
||||
Q_INVOKABLE void compressCurrentChat();
|
||||
Q_INVOKABLE void cancelCompression();
|
||||
|
||||
Q_INVOKABLE void loadAvailableAgentRoles();
|
||||
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
||||
Q_INVOKABLE void openAgentRolesSettings();
|
||||
QStringList availableAgentRoles() const;
|
||||
QString currentAgentRole() const;
|
||||
QString baseSystemPrompt() const;
|
||||
QString currentAgentRoleDescription() const;
|
||||
QString currentAgentRoleSystemPrompt() const;
|
||||
|
||||
Q_INVOKABLE void loadAvailableChatAgents();
|
||||
QStringList availableChatAgents() const;
|
||||
QString currentChatAgent() const;
|
||||
void setCurrentChatAgent(const QString &name);
|
||||
|
||||
int currentMessageTotalEdits() const;
|
||||
int currentMessageAppliedEdits() const;
|
||||
int currentMessagePendingEdits() const;
|
||||
@@ -181,9 +161,8 @@ public:
|
||||
|
||||
QString lastInfoMessage() const;
|
||||
|
||||
bool isThinkingSupport() const;
|
||||
|
||||
bool isCompressing() const;
|
||||
bool canCompress() const;
|
||||
|
||||
bool isInEditor() const;
|
||||
void setInEditor(bool value);
|
||||
@@ -202,7 +181,6 @@ public slots:
|
||||
|
||||
signals:
|
||||
void chatModelChanged();
|
||||
void currentTemplateChanged();
|
||||
void attachmentFilesChanged();
|
||||
void linkedFilesChanged();
|
||||
void inputTokensCountChanged();
|
||||
@@ -218,20 +196,13 @@ signals:
|
||||
|
||||
void lastErrorMessageChanged();
|
||||
void lastInfoMessageChanged();
|
||||
void activeRulesChanged();
|
||||
void activeRulesCountChanged();
|
||||
void sendShortcutTextChanged();
|
||||
|
||||
void useToolsChanged();
|
||||
void useThinkingChanged();
|
||||
void currentMessageEditsStatsChanged();
|
||||
|
||||
void isThinkingSupportChanged();
|
||||
void availableConfigurationsChanged();
|
||||
void currentConfigurationChanged();
|
||||
|
||||
void availableAgentRolesChanged();
|
||||
void currentAgentRoleChanged();
|
||||
void baseSystemPromptChanged();
|
||||
void availableChatAgentsChanged();
|
||||
void currentChatAgentChanged();
|
||||
|
||||
void isCompressingChanged();
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
@@ -251,25 +222,24 @@ private:
|
||||
bool deferSendForAutoCompress(
|
||||
const QString &message,
|
||||
const QStringList &attachments,
|
||||
const QStringList &linkedFiles,
|
||||
bool useTools,
|
||||
bool useThinking);
|
||||
const QStringList &linkedFiles);
|
||||
void dispatchSend(
|
||||
const QString &message,
|
||||
const QStringList &attachments,
|
||||
const QStringList &linkedFiles,
|
||||
bool useTools,
|
||||
bool useThinking);
|
||||
const QStringList &linkedFiles);
|
||||
QString configuredCompressionAgent() const;
|
||||
bool hasImageAttachments(const QStringList &attachments) const;
|
||||
|
||||
SessionFileRegistry *sessionFileRegistry() const;
|
||||
Skills::SkillsManager *skillsManager() const;
|
||||
AgentFactory *agentFactory() const;
|
||||
SessionManager *sessionManager() const;
|
||||
QodeAssist::Session *ownerSession();
|
||||
|
||||
QodeAssist::ConversationHistory *m_history;
|
||||
ChatModel *m_chatModel;
|
||||
PluginLLMCore::PromptProviderChat m_promptProvider;
|
||||
ClientInterface *m_clientInterface;
|
||||
ChatFileManager *m_fileManager;
|
||||
QString m_currentTemplate;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
QStringList m_linkedFiles;
|
||||
@@ -278,8 +248,6 @@ private:
|
||||
QString message;
|
||||
QStringList attachments;
|
||||
QStringList linkedFiles;
|
||||
bool useTools = false;
|
||||
bool useThinking = false;
|
||||
bool active = false;
|
||||
};
|
||||
PendingSend m_pendingSend;
|
||||
@@ -289,13 +257,11 @@ private:
|
||||
QList<Core::IEditor *> m_currentEditors;
|
||||
bool m_isRequestInProgress;
|
||||
QString m_lastErrorMessage;
|
||||
QVariantList m_activeRules;
|
||||
|
||||
|
||||
QString m_lastInfoMessage;
|
||||
|
||||
ChatCompressor *m_chatCompressor;
|
||||
AgentRoleController *m_agentRoleController;
|
||||
ChatConfigurationController *m_configurationController;
|
||||
ChatAgentController *m_agentController;
|
||||
FileEditController *m_fileEditController;
|
||||
InputTokenCounter *m_tokenCounter;
|
||||
ChatHistoryStore *m_historyStore;
|
||||
@@ -303,6 +269,8 @@ private:
|
||||
mutable bool m_sessionFileRegistryResolved = false;
|
||||
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
||||
mutable bool m_skillsManagerResolved = false;
|
||||
mutable QPointer<AgentFactory> m_agentFactory;
|
||||
mutable QPointer<SessionManager> m_sessionManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <memory>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
@@ -12,12 +14,57 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
#include <MessageSerializer.hpp>
|
||||
#include <PluginBlocks.hpp>
|
||||
|
||||
#include "context/ChangesManager.h"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
const QString ChatSerializer::VERSION = "0.2";
|
||||
namespace {
|
||||
|
||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
||||
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
|
||||
|
||||
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
|
||||
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
|
||||
|
||||
void registerEditFromResult(const QString &result)
|
||||
{
|
||||
const int pos = result.indexOf(kFileEditMarker);
|
||||
if (pos < 0)
|
||||
return;
|
||||
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||
if (!doc.isObject())
|
||||
return;
|
||||
const QJsonObject obj = doc.object();
|
||||
const QString editId = obj.value("edit_id").toString();
|
||||
const QString filePath = obj.value("file").toString();
|
||||
if (editId.isEmpty() || filePath.isEmpty())
|
||||
return;
|
||||
Context::ChangesManager::instance().addFileEdit(
|
||||
editId,
|
||||
filePath,
|
||||
obj.value("old_content").toString(),
|
||||
obj.value("new_content").toString(),
|
||||
/*autoApply=*/false,
|
||||
/*isFromHistory=*/true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const QString ChatSerializer::VERSION = "0.3";
|
||||
|
||||
SerializationResult ChatSerializer::saveToFile(
|
||||
const ConversationHistory *history, const QString &filePath)
|
||||
{
|
||||
if (!history)
|
||||
return {false, "No conversation history"};
|
||||
|
||||
if (!ensureDirectoryExists(filePath)) {
|
||||
return {false, "Failed to create directory structure"};
|
||||
}
|
||||
@@ -27,9 +74,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||
}
|
||||
|
||||
QJsonObject root = serializeChat(model, filePath);
|
||||
QJsonDocument doc(root);
|
||||
|
||||
QJsonDocument doc(serializeChat(history));
|
||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||
}
|
||||
@@ -37,8 +82,12 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
|
||||
SerializationResult ChatSerializer::loadFromFile(
|
||||
ConversationHistory *history, const QString &filePath)
|
||||
{
|
||||
if (!history)
|
||||
return {false, "No conversation history"};
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||
@@ -50,180 +99,140 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
||||
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
||||
}
|
||||
|
||||
QJsonObject root = doc.object();
|
||||
QString version = root["version"].toString();
|
||||
|
||||
const QJsonObject root = doc.object();
|
||||
const QString version = root["version"].toString();
|
||||
if (!validateVersion(version)) {
|
||||
return {false, QString("Unsupported version: %1").arg(version)};
|
||||
}
|
||||
|
||||
if (!deserializeChat(model, root, filePath)) {
|
||||
return {false, "Failed to deserialize chat data"};
|
||||
}
|
||||
|
||||
return {true, QString()};
|
||||
if (version == VERSION)
|
||||
return loadCurrent(history, root);
|
||||
return loadLegacy(history, root);
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeMessage(
|
||||
const ChatModel::Message &message, const QString &chatFilePath)
|
||||
{
|
||||
QJsonObject messageObj;
|
||||
messageObj["role"] = static_cast<int>(message.role);
|
||||
messageObj["content"] = message.content;
|
||||
messageObj["id"] = message.id;
|
||||
|
||||
if (message.isRedacted) {
|
||||
messageObj["isRedacted"] = true;
|
||||
}
|
||||
|
||||
if (!message.signature.isEmpty()) {
|
||||
messageObj["signature"] = message.signature;
|
||||
}
|
||||
|
||||
if (message.role == ChatModel::ChatRole::Tool) {
|
||||
if (!message.toolName.isEmpty())
|
||||
messageObj["toolName"] = message.toolName;
|
||||
if (!message.toolArguments.isEmpty())
|
||||
messageObj["toolArguments"] = message.toolArguments;
|
||||
if (!message.toolResult.isEmpty())
|
||||
messageObj["toolResult"] = message.toolResult;
|
||||
}
|
||||
|
||||
if (!message.attachments.isEmpty()) {
|
||||
QJsonArray attachmentsArray;
|
||||
for (const auto &attachment : message.attachments) {
|
||||
QJsonObject attachmentObj;
|
||||
attachmentObj["fileName"] = attachment.filename;
|
||||
attachmentObj["storedPath"] = attachment.content;
|
||||
attachmentsArray.append(attachmentObj);
|
||||
}
|
||||
messageObj["attachments"] = attachmentsArray;
|
||||
}
|
||||
|
||||
if (!message.images.isEmpty()) {
|
||||
QJsonArray imagesArray;
|
||||
for (const auto &image : message.images) {
|
||||
QJsonObject imageObj;
|
||||
imageObj["fileName"] = image.fileName;
|
||||
imageObj["storedPath"] = image.storedPath;
|
||||
imageObj["mediaType"] = image.mediaType;
|
||||
imagesArray.append(imageObj);
|
||||
}
|
||||
messageObj["images"] = imagesArray;
|
||||
}
|
||||
|
||||
if (message.promptTokens > 0 || message.completionTokens > 0) {
|
||||
QJsonObject usageObj;
|
||||
usageObj["promptTokens"] = message.promptTokens;
|
||||
usageObj["completionTokens"] = message.completionTokens;
|
||||
if (message.cachedPromptTokens > 0)
|
||||
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
|
||||
if (message.reasoningTokens > 0)
|
||||
usageObj["reasoningTokens"] = message.reasoningTokens;
|
||||
messageObj["usage"] = usageObj;
|
||||
}
|
||||
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(
|
||||
const QJsonObject &json, const QString &chatFilePath)
|
||||
{
|
||||
ChatModel::Message message;
|
||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||
message.content = json["content"].toString();
|
||||
message.id = json["id"].toString();
|
||||
message.isRedacted = json["isRedacted"].toBool(false);
|
||||
message.signature = json["signature"].toString();
|
||||
message.toolName = json["toolName"].toString();
|
||||
message.toolArguments = json["toolArguments"].toObject();
|
||||
message.toolResult = json["toolResult"].toString();
|
||||
|
||||
if (json.contains("attachments")) {
|
||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
||||
for (const auto &attachmentValue : attachmentsArray) {
|
||||
QJsonObject attachmentObj = attachmentValue.toObject();
|
||||
Context::ContentFile attachment;
|
||||
attachment.filename = attachmentObj["fileName"].toString();
|
||||
attachment.content = attachmentObj["storedPath"].toString();
|
||||
message.attachments.append(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("images")) {
|
||||
QJsonArray imagesArray = json["images"].toArray();
|
||||
for (const auto &imageValue : imagesArray) {
|
||||
QJsonObject imageObj = imageValue.toObject();
|
||||
ChatModel::ImageAttachment image;
|
||||
image.fileName = imageObj["fileName"].toString();
|
||||
image.storedPath = imageObj["storedPath"].toString();
|
||||
image.mediaType = imageObj["mediaType"].toString();
|
||||
message.images.append(image);
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("usage")) {
|
||||
const QJsonObject usageObj = json["usage"].toObject();
|
||||
message.promptTokens = usageObj["promptTokens"].toInt();
|
||||
message.completionTokens = usageObj["completionTokens"].toInt();
|
||||
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
|
||||
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
|
||||
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
|
||||
{
|
||||
QJsonArray messagesArray;
|
||||
for (const auto &message : model->getChatHistory()) {
|
||||
messagesArray.append(serializeMessage(message, chatFilePath));
|
||||
}
|
||||
for (const auto &message : history->messages())
|
||||
messagesArray.append(MessageSerializer::toJson(message));
|
||||
|
||||
QJsonObject root;
|
||||
root["version"] = VERSION;
|
||||
root["messages"] = messagesArray;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
bool ChatSerializer::deserializeChat(
|
||||
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
||||
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
|
||||
{
|
||||
QJsonArray messagesArray = json["messages"].toArray();
|
||||
QVector<ChatModel::Message> messages;
|
||||
messages.reserve(messagesArray.size());
|
||||
history->clear();
|
||||
|
||||
for (const auto &messageValue : messagesArray) {
|
||||
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
|
||||
const QJsonArray messagesArray = root["messages"].toArray();
|
||||
for (const auto &value : messagesArray) {
|
||||
bool ok = false;
|
||||
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
|
||||
if (ok)
|
||||
history->append(std::move(message));
|
||||
}
|
||||
|
||||
model->clear();
|
||||
registerHistoricalFileEdits(history);
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
model->setLoadingFromHistory(true);
|
||||
SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root)
|
||||
{
|
||||
history->clear();
|
||||
|
||||
for (const auto &message : messages) {
|
||||
model->addMessage(
|
||||
message.content,
|
||||
message.role,
|
||||
message.id,
|
||||
message.attachments,
|
||||
message.images,
|
||||
message.isRedacted,
|
||||
message.signature);
|
||||
if (message.role == ChatModel::ChatRole::Tool) {
|
||||
model->setToolMessageData(
|
||||
message.id, message.toolName, message.toolArguments, message.toolResult);
|
||||
const QJsonArray arr = root["messages"].toArray();
|
||||
int i = 0;
|
||||
while (i < arr.size()) {
|
||||
const QJsonObject mj = arr[i].toObject();
|
||||
const auto role = static_cast<LegacyRole>(mj["role"].toInt());
|
||||
|
||||
if (role == LegacyRole::Tool) {
|
||||
Message assistant(Message::Role::Assistant);
|
||||
Message toolResults(Message::Role::User);
|
||||
while (i < arr.size()
|
||||
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) {
|
||||
const QJsonObject tj = arr[i].toObject();
|
||||
const QString toolName = tj["toolName"].toString();
|
||||
const QString id = tj["id"].toString();
|
||||
if (!toolName.isEmpty()) {
|
||||
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||
id, toolName, tj["toolArguments"].toObject()));
|
||||
toolResults.appendBlock(std::make_unique<LLMQore::ToolResultContent>(
|
||||
id, tj["toolResult"].toString()));
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (!assistant.blocks().empty()) {
|
||||
history->append(std::move(assistant));
|
||||
history->append(std::move(toolResults));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
++i;
|
||||
|
||||
if (role == LegacyRole::FileEdit)
|
||||
continue; // derived from the tool result in the new model
|
||||
|
||||
if (role == LegacyRole::Thinking) {
|
||||
const QString content = mj["content"].toString();
|
||||
const QString signature = mj["signature"].toString();
|
||||
Message assistant(Message::Role::Assistant);
|
||||
if (mj["isRedacted"].toBool(false)) {
|
||||
assistant.appendBlock(
|
||||
std::make_unique<LLMQore::RedactedThinkingContent>(signature));
|
||||
} else {
|
||||
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
|
||||
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
|
||||
assistant.appendBlock(
|
||||
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
|
||||
}
|
||||
history->append(std::move(assistant));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role == LegacyRole::User) {
|
||||
Message user(Message::Role::User, mj["id"].toString());
|
||||
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
|
||||
for (const auto &a : mj["attachments"].toArray()) {
|
||||
const QJsonObject ao = a.toObject();
|
||||
user.appendBlock(std::make_unique<StoredAttachmentContent>(
|
||||
ao["fileName"].toString(), ao["storedPath"].toString()));
|
||||
}
|
||||
for (const auto &im : mj["images"].toArray()) {
|
||||
const QJsonObject io = im.toObject();
|
||||
user.appendBlock(std::make_unique<StoredImageContent>(
|
||||
io["fileName"].toString(),
|
||||
io["storedPath"].toString(),
|
||||
io["mediaType"].toString()));
|
||||
}
|
||||
history->append(std::move(user));
|
||||
} else {
|
||||
const QString content = mj["content"].toString();
|
||||
if (content.trimmed().isEmpty())
|
||||
continue;
|
||||
const Message::Role mapped
|
||||
= role == LegacyRole::System ? Message::Role::System : Message::Role::Assistant;
|
||||
Message message(mapped, mj["id"].toString());
|
||||
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
|
||||
history->append(std::move(message));
|
||||
}
|
||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
||||
.arg(message.images.size())
|
||||
.arg(message.isRedacted)
|
||||
.arg(message.signature.length()));
|
||||
}
|
||||
|
||||
model->setLoadingFromHistory(false);
|
||||
registerHistoricalFileEdits(history);
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
return true;
|
||||
void ChatSerializer::registerHistoricalFileEdits(const ConversationHistory *history)
|
||||
{
|
||||
for (const auto &message : history->messages()) {
|
||||
for (const auto &block : message.blocks()) {
|
||||
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
|
||||
registerEditFromResult(tr->result());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||
@@ -235,18 +244,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||
|
||||
bool ChatSerializer::validateVersion(const QString &version)
|
||||
{
|
||||
if (version == VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (version == "0.1") {
|
||||
LOG_MESSAGE(
|
||||
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
||||
"to _content");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return version == VERSION || version == "0.2" || version == "0.1";
|
||||
}
|
||||
|
||||
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||
@@ -302,10 +300,20 @@ bool ChatSerializer::saveContentToStorage(
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath)
|
||||
QString ChatSerializer::loadContentFromStorage(
|
||||
const QString &chatFilePath, const QString &storedPath, StoredContentCache *cache)
|
||||
{
|
||||
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||
QString fullPath = QDir(contentFolder).filePath(storedPath);
|
||||
const QString contentFolder = getChatContentFolder(chatFilePath);
|
||||
const QString fullPath = QDir(contentFolder).filePath(storedPath);
|
||||
|
||||
const QFileInfo info(fullPath);
|
||||
if (cache) {
|
||||
const auto it = cache->constFind(fullPath);
|
||||
if (it != cache->constEnd() && it->modified == info.lastModified()
|
||||
&& it->size == info.size()) {
|
||||
return it->base64;
|
||||
}
|
||||
}
|
||||
|
||||
QFile file(fullPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
@@ -313,10 +321,12 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
|
||||
return QString();
|
||||
}
|
||||
|
||||
QByteArray contentData = file.readAll();
|
||||
file.close();
|
||||
const QString base64 = QString::fromUtf8(file.readAll().toBase64());
|
||||
|
||||
return contentData.toBase64();
|
||||
if (cache)
|
||||
cache->insert(fullPath, {info.lastModified(), info.size(), base64});
|
||||
|
||||
return base64;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
namespace QodeAssist {
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@@ -17,29 +21,41 @@ struct SerializationResult
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
struct StoredContentEntry
|
||||
{
|
||||
QDateTime modified;
|
||||
qint64 size = 0;
|
||||
QString base64;
|
||||
};
|
||||
|
||||
using StoredContentCache = QHash<QString, StoredContentEntry>;
|
||||
|
||||
class ChatSerializer
|
||||
{
|
||||
public:
|
||||
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
|
||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
||||
|
||||
// Public for testing purposes
|
||||
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
|
||||
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
|
||||
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
||||
static SerializationResult saveToFile(
|
||||
const ConversationHistory *history, const QString &filePath);
|
||||
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
|
||||
|
||||
// Content management (images and text files)
|
||||
static QString getChatContentFolder(const QString &chatFilePath);
|
||||
static bool saveContentToStorage(const QString &chatFilePath,
|
||||
const QString &fileName,
|
||||
const QString &base64Data,
|
||||
QString &storedPath);
|
||||
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
||||
static bool saveContentToStorage(
|
||||
const QString &chatFilePath,
|
||||
const QString &fileName,
|
||||
const QString &base64Data,
|
||||
QString &storedPath);
|
||||
static QString loadContentFromStorage(
|
||||
const QString &chatFilePath,
|
||||
const QString &storedPath,
|
||||
StoredContentCache *cache = nullptr);
|
||||
|
||||
private:
|
||||
static const QString VERSION;
|
||||
static constexpr int CURRENT_VERSION = 1;
|
||||
|
||||
static QJsonObject serializeChat(const ConversationHistory *history);
|
||||
static SerializationResult loadCurrent(ConversationHistory *history, const QJsonObject &root);
|
||||
static SerializationResult loadLegacy(ConversationHistory *history, const QJsonObject &root);
|
||||
static void registerHistoricalFileEdits(const ConversationHistory *history);
|
||||
|
||||
static bool ensureDirectoryExists(const QString &filePath);
|
||||
static bool validateVersion(const QString &version);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatUtils.h"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatView.hpp"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ChatWidget.hpp"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,29 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QStringList>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "pluginllmcore/IPromptProvider.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include <ErrorInfo.hpp>
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <ResponseEvent.hpp>
|
||||
#include <context/ContextManager.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
class SessionManager;
|
||||
class Session;
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
@@ -25,26 +35,29 @@ class ClientInterface : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClientInterface(
|
||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setHistory(ConversationHistory *history);
|
||||
void setActiveAgent(const QString &agentName);
|
||||
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
const QList<QString> &linkedFiles = {},
|
||||
bool useTools = false,
|
||||
bool useThinking = false);
|
||||
const QList<QString> &linkedFiles = {});
|
||||
void clearMessages();
|
||||
void cancelRequest();
|
||||
|
||||
Context::ContextManager *contextManager() const;
|
||||
|
||||
|
||||
void setChatFilePath(const QString &filePath);
|
||||
QString chatFilePath() const;
|
||||
|
||||
void ensureSession();
|
||||
Session *session() const;
|
||||
|
||||
signals:
|
||||
void errorOccurred(const QString &error);
|
||||
void messageReceivedCompletely();
|
||||
@@ -52,50 +65,38 @@ signals:
|
||||
void messageUsageReceived(
|
||||
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
||||
|
||||
private slots:
|
||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
void handleThinkingBlockReceived(
|
||||
const QString &requestId, const QString &thinking, const QString &signature);
|
||||
void handleToolExecutionStarted(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &arguments);
|
||||
void handleToolExecutionCompleted(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QString &toolOutput);
|
||||
|
||||
private:
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||
QString getCurrentFileContext() const;
|
||||
QString getSystemPromptWithLinkedFiles(
|
||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
||||
bool ensureAgentBound();
|
||||
|
||||
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
|
||||
void onSessionFinished(const QString &requestId);
|
||||
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||
|
||||
QStringList invokedSkillNames(const QString &message) const;
|
||||
QString buildChatContextLayer() const;
|
||||
QString requestIdForSession(Session *session) const;
|
||||
bool isImageFile(const QString &filePath) const;
|
||||
QString getMediaTypeForImage(const QString &filePath) const;
|
||||
QString encodeImageToBase64(const QString &filePath) const;
|
||||
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
PluginLLMCore::Provider *provider;
|
||||
bool dropPreToolText = false;
|
||||
QPointer<Session> session;
|
||||
};
|
||||
|
||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
QPointer<ConversationHistory> m_history;
|
||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QPointer<Session> m_session;
|
||||
QString m_activeAgent;
|
||||
QString m_boundAgent;
|
||||
QString m_chatFilePath;
|
||||
std::shared_ptr<StoredContentCache> m_contentCache;
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
QHash<QString, QString> m_accumulatedResponses;
|
||||
QSet<QString> m_awaitingContinuation;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "FileEditController.hpp"
|
||||
|
||||
@@ -9,15 +10,13 @@
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "context/ChangesManager.h"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
|
||||
FileEditController::FileEditController(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
{
|
||||
auto &changes = Context::ChangesManager::instance();
|
||||
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
||||
@@ -79,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
|
||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||
emit infoMessage(QString("File edit applied successfully"));
|
||||
updateFileEditStatus(editId, "applied");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
emit errorOccurred(
|
||||
@@ -94,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
|
||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||
emit infoMessage(QString("File edit rejected"));
|
||||
updateFileEditStatus(editId, "rejected");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
emit errorOccurred(
|
||||
@@ -109,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
|
||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||
emit infoMessage(QString("File edit undone successfully"));
|
||||
updateFileEditStatus(editId, "rejected");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
emit errorOccurred(
|
||||
@@ -162,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
|
||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||
}
|
||||
|
||||
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
|
||||
{
|
||||
auto messages = m_chatModel->getChatHistory();
|
||||
for (int i = 0; i < messages.size(); ++i) {
|
||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
||||
QString content = messages[i].content;
|
||||
|
||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||
int markerPos = content.indexOf(marker);
|
||||
|
||||
QString jsonStr = content;
|
||||
if (markerPos >= 0) {
|
||||
jsonStr = content.mid(markerPos + marker.length());
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
QJsonObject obj = doc.object();
|
||||
obj["status"] = status;
|
||||
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
if (!edit.statusMessage.isEmpty()) {
|
||||
obj["status_message"] = edit.statusMessage;
|
||||
}
|
||||
|
||||
QString updatedContent = marker
|
||||
+ QString::fromUtf8(
|
||||
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateStats();
|
||||
}
|
||||
|
||||
void FileEditController::applyAllForCurrentMessage()
|
||||
{
|
||||
if (m_currentRequestId.isEmpty()) {
|
||||
@@ -222,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
|
||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
||||
}
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Applied) {
|
||||
updateFileEditStatus(edit.editId, "applied");
|
||||
}
|
||||
}
|
||||
|
||||
updateStats();
|
||||
}
|
||||
|
||||
@@ -254,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
|
||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
||||
}
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Rejected) {
|
||||
updateFileEditStatus(edit.editId, "rejected");
|
||||
}
|
||||
}
|
||||
|
||||
updateStats();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -8,14 +9,12 @@
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel;
|
||||
|
||||
class FileEditController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
explicit FileEditController(QObject *parent = nullptr);
|
||||
|
||||
void setCurrentRequestId(const QString &requestId);
|
||||
void clearCurrentRequestId();
|
||||
@@ -40,9 +39,6 @@ signals:
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
QString m_currentRequestId;
|
||||
int m_totalEdits{0};
|
||||
int m_appliedEdits{0};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "FileItem.hpp"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "FileMentionItem.hpp"
|
||||
|
||||
@@ -87,22 +88,6 @@ void FileMentionItem::moveDown()
|
||||
}
|
||||
}
|
||||
|
||||
void FileMentionItem::selectCurrent()
|
||||
{
|
||||
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
|
||||
return;
|
||||
|
||||
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
|
||||
if (item.value("isProject").toBool()) {
|
||||
emit projectSelected(item.value("projectName").toString());
|
||||
} else {
|
||||
emit fileSelected(
|
||||
item.value("absolutePath").toString(),
|
||||
item.value("relativePath").toString(),
|
||||
item.value("projectName").toString());
|
||||
}
|
||||
}
|
||||
|
||||
void FileMentionItem::dismiss()
|
||||
{
|
||||
m_searchResults.clear();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -28,7 +29,6 @@ public:
|
||||
Q_INVOKABLE void refreshSearch();
|
||||
Q_INVOKABLE void moveUp();
|
||||
Q_INVOKABLE void moveDown();
|
||||
Q_INVOKABLE void selectCurrent();
|
||||
Q_INVOKABLE void dismiss();
|
||||
|
||||
Q_INVOKABLE QVariantMap handleFileSelection(
|
||||
|
||||
@@ -1,52 +1,23 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "InputTokenCounter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "context/TokenUtils.hpp"
|
||||
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Message.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
InputTokenCounter::InputTokenCounter(
|
||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
|
||||
ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
, m_history(history)
|
||||
, m_contextManager(contextManager)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
connect(
|
||||
&settings.useSystemPrompt,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&InputTokenCounter::recompute);
|
||||
connect(
|
||||
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
|
||||
connect(
|
||||
&settings.enableChatTools,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&InputTokenCounter::recompute);
|
||||
|
||||
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
});
|
||||
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
}
|
||||
|
||||
@@ -73,33 +44,8 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
||||
recompute();
|
||||
}
|
||||
|
||||
void InputTokenCounter::rewireToolsChangedConnection()
|
||||
{
|
||||
if (m_toolsChangedConn)
|
||||
QObject::disconnect(m_toolsChangedConn);
|
||||
m_toolsChangedConn = {};
|
||||
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
if (!provider)
|
||||
return;
|
||||
auto *tm = provider->toolsManager();
|
||||
if (!tm)
|
||||
return;
|
||||
|
||||
m_toolsChangedConn = connect(
|
||||
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
||||
}
|
||||
|
||||
void InputTokenCounter::recompute()
|
||||
{
|
||||
int inputTokens = m_messageTokens;
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
if (settings.useSystemPrompt()) {
|
||||
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
||||
}
|
||||
|
||||
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
|
||||
int imageTokens = 0;
|
||||
for (const QString &p : paths) {
|
||||
@@ -111,15 +57,24 @@ void InputTokenCounter::recompute()
|
||||
return imageTokens;
|
||||
};
|
||||
|
||||
int pendingTokens = m_messageTokens;
|
||||
if (!m_attachments.isEmpty()) {
|
||||
QStringList textPaths;
|
||||
inputTokens += splitImageEstimate(m_attachments, textPaths);
|
||||
pendingTokens += splitImageEstimate(m_attachments, textPaths);
|
||||
if (!textPaths.isEmpty()) {
|
||||
auto attachFiles = m_contextManager->getContentFiles(textPaths);
|
||||
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||
pendingTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_hasServerUsage && m_history && !m_history->isEmpty()) {
|
||||
m_inputTokens = m_serverInputTokens + pendingTokens;
|
||||
emit inputTokensChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
int inputTokens = pendingTokens;
|
||||
|
||||
if (!m_linkedFiles.isEmpty()) {
|
||||
QStringList textPaths;
|
||||
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
|
||||
@@ -129,54 +84,32 @@ void InputTokenCounter::recompute()
|
||||
}
|
||||
}
|
||||
|
||||
const auto &history = m_chatModel->getChatHistory();
|
||||
for (const auto &message : history) {
|
||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
||||
inputTokens += 4; // + role
|
||||
}
|
||||
|
||||
if (settings.enableChatTools()) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
||||
providerName)) {
|
||||
if (auto *tm = provider->toolsManager()) {
|
||||
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
||||
if (!toolDefs.isEmpty()) {
|
||||
const QByteArray serialized
|
||||
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
||||
inputTokens += static_cast<int>(serialized.size() / 4);
|
||||
}
|
||||
}
|
||||
if (m_history) {
|
||||
for (const auto &message : m_history->messages()) {
|
||||
inputTokens += Context::TokenUtils::estimateTokens(message.text());
|
||||
inputTokens += 4; // + role
|
||||
}
|
||||
}
|
||||
|
||||
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
||||
m_inputTokens = inputTokens;
|
||||
emit inputTokensChanged();
|
||||
}
|
||||
|
||||
void InputTokenCounter::recordSent()
|
||||
void InputTokenCounter::recordServerUsage(int promptTokens, int cachedTokens)
|
||||
{
|
||||
m_lastSentEstimate = m_calibrationFactor > 0.0
|
||||
? static_cast<int>(m_inputTokens / m_calibrationFactor)
|
||||
: m_inputTokens;
|
||||
}
|
||||
|
||||
void InputTokenCounter::recordServerUsage(int promptTokens)
|
||||
{
|
||||
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
|
||||
const int serverInput = promptTokens + cachedTokens;
|
||||
if (serverInput <= 0)
|
||||
return;
|
||||
|
||||
const double rawFactor
|
||||
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
|
||||
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
|
||||
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped;
|
||||
|
||||
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
|
||||
.arg(promptTokens)
|
||||
.arg(m_lastSentEstimate)
|
||||
.arg(rawFactor, 0, 'f', 3)
|
||||
.arg(m_calibrationFactor, 0, 'f', 3));
|
||||
m_serverInputTokens = serverInput;
|
||||
m_hasServerUsage = true;
|
||||
recompute();
|
||||
}
|
||||
|
||||
void InputTokenCounter::resetServerUsage()
|
||||
{
|
||||
m_serverInputTokens = 0;
|
||||
m_hasServerUsage = false;
|
||||
recompute();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist {
|
||||
class ConversationHistory;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
class ContextManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel;
|
||||
|
||||
class InputTokenCounter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
InputTokenCounter(
|
||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr);
|
||||
ConversationHistory *history,
|
||||
Context::ContextManager *contextManager,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
int inputTokens() const;
|
||||
|
||||
@@ -29,25 +34,22 @@ public:
|
||||
void setLinkedFiles(const QStringList &linkedFiles);
|
||||
void recompute();
|
||||
|
||||
void recordSent();
|
||||
void recordServerUsage(int promptTokens);
|
||||
void recordServerUsage(int promptTokens, int cachedTokens);
|
||||
void resetServerUsage();
|
||||
|
||||
signals:
|
||||
void inputTokensChanged();
|
||||
|
||||
private:
|
||||
void rewireToolsChangedConnection();
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
ConversationHistory *m_history;
|
||||
Context::ContextManager *m_contextManager;
|
||||
QMetaObject::Connection m_toolsChangedConn;
|
||||
|
||||
QStringList m_attachments;
|
||||
QStringList m_linkedFiles;
|
||||
int m_messageTokens{0};
|
||||
int m_inputTokens{0};
|
||||
int m_lastSentEstimate{0};
|
||||
double m_calibrationFactor{1.0};
|
||||
int m_serverInputTokens{0};
|
||||
bool m_hasServerUsage{false};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "SessionFileRegistry.hpp"
|
||||
|
||||
@@ -7,29 +8,43 @@
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <Session.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
SessionFileRegistry::~SessionFileRegistry() = default;
|
||||
|
||||
bool SessionFileRegistry::isLocked(const QString &path) const
|
||||
{
|
||||
return !path.isEmpty() && m_lockedPaths.contains(path);
|
||||
return !path.isEmpty() && !m_locks.value(path).isNull();
|
||||
}
|
||||
|
||||
bool SessionFileRegistry::lock(const QString &path)
|
||||
bool SessionFileRegistry::isLockedByOther(const QString &path, QodeAssist::Session *self) const
|
||||
{
|
||||
if (path.isEmpty() || m_lockedPaths.contains(path)) {
|
||||
if (path.isEmpty())
|
||||
return false;
|
||||
}
|
||||
m_lockedPaths.insert(path);
|
||||
const auto owner = m_locks.value(path);
|
||||
return !owner.isNull() && owner != self;
|
||||
}
|
||||
|
||||
bool SessionFileRegistry::lock(const QString &path, QodeAssist::Session *owner)
|
||||
{
|
||||
if (path.isEmpty())
|
||||
return false;
|
||||
const auto existing = m_locks.value(path);
|
||||
if (!existing.isNull() && existing != owner)
|
||||
return false;
|
||||
m_locks.insert(path, owner);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionFileRegistry::release(const QString &path)
|
||||
{
|
||||
m_lockedPaths.remove(path);
|
||||
m_locks.remove(path);
|
||||
}
|
||||
|
||||
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
||||
@@ -44,7 +59,7 @@ QString SessionFileRegistry::takePendingChatFile()
|
||||
|
||||
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
||||
{
|
||||
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
|
||||
if (desiredPath.isEmpty() || !isLocked(desiredPath)) {
|
||||
return desiredPath;
|
||||
}
|
||||
|
||||
@@ -58,7 +73,7 @@ QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
||||
if (!suffix.isEmpty()) {
|
||||
candidate += '.' + suffix;
|
||||
}
|
||||
if (!m_lockedPaths.contains(candidate)) {
|
||||
if (!isLocked(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist {
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
// Shared registry of chat session (autosave) file paths that are currently held by a live
|
||||
// chat instance. Lets every chat view — bottom pane, navigation panel, editor split — claim
|
||||
// a unique history file so two sessions never autosave into the same path.
|
||||
// Shared registry mapping each chat (autosave) file to the live Session that owns it, so a
|
||||
// file is busy only while its owning Session is alive (a destroyed Session frees it — the
|
||||
// QPointer goes null). Keeps two chat views from autosaving into the same path.
|
||||
class SessionFileRegistry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SessionFileRegistry(QObject *parent = nullptr);
|
||||
~SessionFileRegistry() override;
|
||||
|
||||
bool isLocked(const QString &path) const;
|
||||
bool lock(const QString &path);
|
||||
bool isLockedByOther(const QString &path, QodeAssist::Session *self) const;
|
||||
bool lock(const QString &path, QodeAssist::Session *owner);
|
||||
void release(const QString &path);
|
||||
|
||||
QString uniqueFreePath(const QString &desiredPath) const;
|
||||
@@ -31,7 +39,7 @@ public:
|
||||
QString takePendingChatFile();
|
||||
|
||||
private:
|
||||
QSet<QString> m_lockedPaths;
|
||||
QHash<QString, QPointer<QodeAssist::Session>> m_locks;
|
||||
QString m_pendingChatFile;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 |
@@ -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 |
5
ChatView/icons/warning-icon.svg
Normal file
5
ChatView/icons/warning-icon.svg
Normal 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 |
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@@ -19,6 +20,9 @@ ChatRootView {
|
||||
colorGroup: SystemPalette.Active
|
||||
}
|
||||
|
||||
property bool hasActiveError: false
|
||||
readonly property color errorColor: "#d32f2f"
|
||||
|
||||
palette {
|
||||
window: sysPalette.window
|
||||
windowText: sysPalette.windowText
|
||||
@@ -114,7 +118,6 @@ ChatRootView {
|
||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
}
|
||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||
contextButton.onClicked: contextViewer.open()
|
||||
pinButton {
|
||||
visible: typeof _chatview !== 'undefined'
|
||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||
@@ -134,43 +137,18 @@ ChatRootView {
|
||||
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
||||
? qsTr("Move this chat to an editor tab")
|
||||
: qsTr("Move this chat to a separate window")
|
||||
toolsButton {
|
||||
checked: root.useTools
|
||||
onCheckedChanged: {
|
||||
root.useTools = toolsButton.checked
|
||||
}
|
||||
}
|
||||
thinkingMode {
|
||||
checked: root.useThinking
|
||||
enabled: root.isThinkingSupport
|
||||
onCheckedChanged: {
|
||||
root.useThinking = thinkingMode.checked
|
||||
}
|
||||
}
|
||||
settingsButton.onClicked: root.openSettings()
|
||||
configSelector {
|
||||
model: root.availableConfigurations
|
||||
displayText: root.currentConfiguration
|
||||
agentSelector {
|
||||
model: root.availableChatAgents
|
||||
displayText: root.currentChatAgent
|
||||
onActivated: function(index) {
|
||||
if (index > 0) {
|
||||
root.applyConfiguration(root.availableConfigurations[index])
|
||||
}
|
||||
root.currentChatAgent = root.availableChatAgents[index]
|
||||
}
|
||||
|
||||
Component.onCompleted: root.loadAvailableChatAgents()
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableConfigurations()
|
||||
}
|
||||
}
|
||||
|
||||
roleSelector {
|
||||
model: root.availableAgentRoles
|
||||
displayText: root.currentAgentRole
|
||||
onActivated: function(index) {
|
||||
root.applyAgentRole(root.availableAgentRoles[index])
|
||||
}
|
||||
|
||||
popup.onAboutToShow: {
|
||||
root.loadAvailableAgentRoles()
|
||||
root.loadAvailableChatAgents()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,11 +389,10 @@ ChatRootView {
|
||||
QQC.TextArea {
|
||||
id: messageInput
|
||||
|
||||
placeholderText: Qt.platform.os === "osx"
|
||||
? qsTr("Type your message here... (⌘+↩ to send)")
|
||||
: qsTr("Type your message here... (Ctrl+Enter to send)")
|
||||
placeholderText: qsTr("Type your message here... (%1 to send)").arg(root.sendShortcutText)
|
||||
placeholderTextColor: palette.mid
|
||||
color: palette.text
|
||||
wrapMode: TextArea.Wrap
|
||||
background: Rectangle {
|
||||
radius: 2
|
||||
color: palette.base
|
||||
@@ -494,6 +471,9 @@ ChatRootView {
|
||||
skillCommandPopup.dismiss()
|
||||
event.accepted = true
|
||||
}
|
||||
} else if (root.isSendShortcut(event.key, event.modifiers)) {
|
||||
root.sendChatMessage()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,13 +566,25 @@ ChatRootView {
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
isCompressing: root.isCompressing
|
||||
isProcessing: root.isRequestInProgress
|
||||
canCompress: root.canCompress
|
||||
canSend: root.currentChatAgent !== ""
|
||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||
: root.cancelRequest()
|
||||
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
|
||||
sendButtonTooltip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
||||
: qsTr("Stop")
|
||||
sendButton.icon.source: root.isRequestInProgress
|
||||
? ""
|
||||
: (root.hasActiveError ? "qrc:/qt/qml/ChatView/icons/warning-icon.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/chat-icon.svg")
|
||||
sendButton.text: root.isRequestInProgress ? qsTr("Stop") : qsTr("Send")
|
||||
sendButton.accentColor: (root.hasActiveError && !root.isRequestInProgress)
|
||||
? root.errorColor : "transparent"
|
||||
sendButtonTooltip.text: root.isRequestInProgress
|
||||
? qsTr("Stop")
|
||||
: (root.currentChatAgent === ""
|
||||
? qsTr("Assign a chat agent in the Pipelines settings")
|
||||
: (root.hasActiveError
|
||||
? root.lastErrorMessage
|
||||
: qsTr("Send message to LLM %1").arg(root.sendShortcutText)))
|
||||
compressButton.onClicked: compressConfirmDialog.open()
|
||||
cancelCompressButton.onClicked: root.cancelCompression()
|
||||
syncOpenFiles {
|
||||
@@ -667,6 +659,7 @@ ChatRootView {
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
root.hasActiveError = false
|
||||
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||
messageInput.text = ""
|
||||
fileMentionPopup.clearMentions()
|
||||
@@ -689,13 +682,122 @@ ChatRootView {
|
||||
onAccepted: root.compressCurrentChat()
|
||||
}
|
||||
|
||||
Toast {
|
||||
id: errorToast
|
||||
z: 1000
|
||||
Rectangle {
|
||||
id: errorBanner
|
||||
|
||||
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
|
||||
border.color: Qt.darker(infoToast.color, 1.3)
|
||||
toastTextColor: "#FFFFFF"
|
||||
z: 1000
|
||||
visible: root.hasActiveError && root.lastErrorMessage.length > 0
|
||||
|
||||
width: parent.width / 2
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: 10
|
||||
anchors.bottomMargin: bottomBar.height + 48
|
||||
|
||||
height: visible ? errorRow.implicitHeight + 12 : 0
|
||||
|
||||
color: Qt.rgba(0.83, 0.18, 0.18, 0.96)
|
||||
radius: 6
|
||||
border.color: Qt.darker(color, 1.3)
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
id: errorRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 6
|
||||
spacing: 8
|
||||
|
||||
TextEdit {
|
||||
Layout.fillWidth: true
|
||||
text: root.lastErrorMessage
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: 12
|
||||
wrapMode: TextEdit.Wrap
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
selectionColor: Qt.darker(errorBanner.color, 1.3)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: copyErrorButton
|
||||
|
||||
property bool copied: false
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
implicitWidth: copyErrorLabel.implicitWidth + 18
|
||||
implicitHeight: 22
|
||||
radius: 4
|
||||
color: copyErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28)
|
||||
: Qt.rgba(1, 1, 1, 0.16)
|
||||
border.color: Qt.rgba(1, 1, 1, 0.45)
|
||||
border.width: 1
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
id: copyErrorLabel
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: copyErrorButton.copied ? qsTr("Copied") : qsTr("Copy")
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: copyErrorMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.copyToClipboard(root.lastErrorMessage)
|
||||
copyErrorButton.copied = true
|
||||
copyErrorResetTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyErrorResetTimer
|
||||
|
||||
interval: 1500
|
||||
onTriggered: copyErrorButton.copied = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: closeErrorButton
|
||||
|
||||
Layout.alignment: Qt.AlignTop
|
||||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
radius: 4
|
||||
color: closeErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28) : "transparent"
|
||||
border.color: Qt.rgba(1, 1, 1, 0.45)
|
||||
border.width: closeErrorMouse.containsMouse ? 1 : 0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "✕"
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeErrorMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.hasActiveError = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toast {
|
||||
@@ -707,35 +809,11 @@ ChatRootView {
|
||||
toastTextColor: "#FFFFFF"
|
||||
}
|
||||
|
||||
ContextViewer {
|
||||
id: contextViewer
|
||||
|
||||
width: Math.min(parent.width * 0.85, 800)
|
||||
height: Math.min(parent.height * 0.85, 700)
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
|
||||
baseSystemPrompt: root.baseSystemPrompt
|
||||
currentAgentRole: root.currentAgentRole
|
||||
currentAgentRoleDescription: root.currentAgentRoleDescription
|
||||
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
||||
activeRules: root.activeRules
|
||||
activeRulesCount: root.activeRulesCount
|
||||
|
||||
onOpenSettings: root.openSettings()
|
||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||
onOpenRulesFolder: root.openRulesFolder()
|
||||
onRefreshRules: root.refreshRules()
|
||||
onRuleSelected: function(index) {
|
||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onLastErrorMessageChanged() {
|
||||
if (root.lastErrorMessage.length > 0) {
|
||||
errorToast.show(root.lastErrorMessage)
|
||||
root.hasActiveError = true
|
||||
}
|
||||
}
|
||||
function onLastInfoMessageChanged() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import ChatView
|
||||
@@ -355,10 +356,9 @@ Rectangle {
|
||||
smooth: true
|
||||
mipmap: true
|
||||
|
||||
BusyIndicator {
|
||||
QoABusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: imageDisplay.status === Image.Loading
|
||||
visible: running
|
||||
}
|
||||
|
||||
Text {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@@ -19,6 +20,9 @@ Rectangle {
|
||||
property alias cancelCompressButton: cancelCompressButtonId
|
||||
|
||||
property bool isCompressing: false
|
||||
property bool isProcessing: false
|
||||
property bool canCompress: true
|
||||
property bool canSend: true
|
||||
property alias sendButtonTooltip: sendButtonTooltipId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
@@ -107,7 +111,7 @@ Rectangle {
|
||||
visible: root.isCompressing
|
||||
spacing: 6
|
||||
|
||||
BusyIndicator {
|
||||
QoABusyIndicator {
|
||||
id: compressBusyIndicator
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -137,37 +141,78 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: compressButtonId
|
||||
Item {
|
||||
id: compressButtonContainer
|
||||
|
||||
visible: !root.isCompressing
|
||||
text: qsTr("Compress")
|
||||
implicitWidth: compressButtonId.implicitWidth
|
||||
implicitHeight: compressButtonId.implicitHeight
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
QoAButton {
|
||||
id: compressButtonId
|
||||
|
||||
anchors.fill: parent
|
||||
enabled: root.canCompress
|
||||
text: qsTr("Compress")
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: compressHoverHandler
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
visible: compressButtonId.hovered
|
||||
visible: compressHoverHandler.hovered
|
||||
delay: 250
|
||||
text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||
text: root.canCompress
|
||||
? qsTr("Compress chat (create summarized copy using LLM)")
|
||||
: qsTr("Assign a compression agent in the Pipelines settings")
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: sendButtonId
|
||||
Item {
|
||||
id: sendButtonContainer
|
||||
|
||||
icon {
|
||||
height: 15
|
||||
width: 15
|
||||
implicitWidth: sendButtonId.implicitWidth
|
||||
implicitHeight: sendButtonId.implicitHeight
|
||||
|
||||
QoAButton {
|
||||
id: sendButtonId
|
||||
|
||||
anchors.fill: parent
|
||||
enabled: root.isProcessing || root.canSend
|
||||
leftPadding: root.isProcessing ? 22 : 4
|
||||
|
||||
icon {
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
|
||||
QoABusyIndicator {
|
||||
id: sendBusyIndicator
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 14
|
||||
height: 14
|
||||
running: root.isProcessing
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: sendHoverHandler
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
id: sendButtonTooltipId
|
||||
|
||||
visible: sendButtonId.hovered
|
||||
visible: sendHoverHandler.hovered
|
||||
delay: 250
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
@@ -21,12 +22,8 @@ Rectangle {
|
||||
property alias openChatHistory: openChatHistoryId
|
||||
property alias pinButton: pinButtonId
|
||||
property alias relocateButton: relocateButtonId
|
||||
property alias contextButton: contextButtonId
|
||||
property alias toolsButton: toolsButtonId
|
||||
property alias thinkingMode: thinkingModeId
|
||||
property alias settingsButton: settingsButtonId
|
||||
property alias configSelector: configSelectorId
|
||||
property alias roleSelector: roleSelector
|
||||
property alias agentSelector: agentSelectorId
|
||||
property alias relocateTooltip: relocateTooltipId
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
@@ -133,7 +130,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: configSelectorId
|
||||
id: agentSelectorId
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
@@ -141,87 +138,17 @@ Rectangle {
|
||||
currentIndex: 0
|
||||
|
||||
QoAToolTip {
|
||||
visible: configSelectorId.hovered
|
||||
visible: agentSelectorId.hovered
|
||||
delay: 250
|
||||
text: qsTr("Switch saved AI configuration")
|
||||
text: qsTr("Select chat agent (provider and model come from the agent)")
|
||||
}
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: roleSelector
|
||||
|
||||
implicitHeight: 25
|
||||
|
||||
model: []
|
||||
currentIndex: 0
|
||||
|
||||
QoAToolTip {
|
||||
visible: roleSelector.hovered
|
||||
delay: 250
|
||||
text: qsTr("Switch agent role (different system prompts)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: toolsButtonId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
checkable: true
|
||||
opacity: enabled ? 1.0 : 0.2
|
||||
|
||||
icon {
|
||||
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
visible: toolsButtonId.hovered
|
||||
delay: 250
|
||||
text: {
|
||||
if (!toolsButtonId.enabled) {
|
||||
return qsTr("Tools are disabled in General Settings")
|
||||
}
|
||||
return toolsButtonId.checked
|
||||
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
||||
: qsTr("Tools disabled: Simple conversation without tool access")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: thinkingModeId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
checkable: true
|
||||
opacity: enabled ? 1.0 : 0.2
|
||||
|
||||
icon {
|
||||
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
visible: thinkingModeId.hovered
|
||||
delay: 250
|
||||
text: thinkingModeId.enabled
|
||||
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
|
||||
: qsTr("Thinking Mode disabled"))
|
||||
: qsTr("Thinking Mode is not available for this provider")
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: settingsButtonId
|
||||
|
||||
@@ -331,23 +258,6 @@ Rectangle {
|
||||
|
||||
QoASeparator {}
|
||||
|
||||
QoAButton {
|
||||
id: contextButtonId
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
|
||||
QoAToolTip {
|
||||
visible: contextButtonId.hovered
|
||||
delay: 250
|
||||
text: qsTr("View chat context (system prompt, role, rules)")
|
||||
}
|
||||
}
|
||||
|
||||
Badge {
|
||||
id: tokensBadgeId
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CodeHandler.hpp"
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
@@ -208,24 +209,4 @@ QString CodeHandler::detectLanguageFromExtension(const QString &extension)
|
||||
return extensionToLanguage.value(extension.toLower(), "");
|
||||
}
|
||||
|
||||
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
|
||||
{
|
||||
static const QRegularExpression
|
||||
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||
return regex;
|
||||
}
|
||||
|
||||
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
|
||||
{
|
||||
static const QRegularExpression
|
||||
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
|
||||
return regex;
|
||||
}
|
||||
|
||||
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
|
||||
{
|
||||
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
||||
return regex;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -31,10 +32,6 @@ public:
|
||||
|
||||
private:
|
||||
static QString getCommentPrefix(const QString &language);
|
||||
|
||||
static const QRegularExpression &getFullCodeBlockRegex();
|
||||
static const QRegularExpression &getPartialStartBlockRegex();
|
||||
static const QRegularExpression &getPartialEndBlockRegex();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -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
|
||||
@@ -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
21
LICENSE
@@ -1,3 +1,24 @@
|
||||
===============================================================
|
||||
ADDITIONAL TERMS UNDER GPLv3 SECTION 7(b)
|
||||
===============================================================
|
||||
|
||||
In accordance with Section 7(b) of the GNU General Public License v3.0,
|
||||
the following additional attribution term applies to QodeAssist:
|
||||
|
||||
You must preserve all author attributions, copyright notices, and the
|
||||
project name "QodeAssist" in all copies and modified versions,
|
||||
including in source file headers, the plugin metadata
|
||||
(QodeAssist.json.in), and the About dialog or equivalent user-facing
|
||||
identification. Modified versions must be clearly marked as different
|
||||
from the original.
|
||||
|
||||
This is a reasonable attribution requirement permitted under GPLv3
|
||||
§7(b) and §7(c). It supplements the notice-preservation obligations of
|
||||
§4 and §5.
|
||||
|
||||
Copyright (C) 2024-2026 Petr Mironychev
|
||||
===============================================================
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
|
||||
@@ -1,39 +1,61 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include "sources/common/ContextData.hpp"
|
||||
#include <Agent.hpp>
|
||||
#include <AgentConfig.hpp>
|
||||
#include <AgentFactory.hpp>
|
||||
#include <AgentRouter.hpp>
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <PluginBlocks.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <SystemPromptBuilder.hpp>
|
||||
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "CodeHandler.hpp"
|
||||
#include "context/DocumentContextReader.hpp"
|
||||
#include "context/Utils.hpp"
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include <pluginllmcore/RulesLoader.hpp>
|
||||
#include "sources/settings/PipelinesConfig.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMClientInterface::LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||
PluginLLMCore::IPromptProvider *promptProvider,
|
||||
AgentFactory &agentFactory,
|
||||
SessionManager &sessionManager,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger)
|
||||
: m_generalSettings(generalSettings)
|
||||
, m_completeSettings(completeSettings)
|
||||
, m_providerRegistry(providerRegistry)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_agentFactory(agentFactory)
|
||||
, m_sessionManager(sessionManager)
|
||||
, m_documentReader(documentReader)
|
||||
, m_performanceLogger(performanceLogger)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{
|
||||
}
|
||||
{}
|
||||
|
||||
LLMClientInterface::~LLMClientInterface()
|
||||
{
|
||||
@@ -50,58 +72,56 @@ void LLMClientInterface::startImpl()
|
||||
emit started();
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||
void LLMClientInterface::onCompletionFinished(const QString &requestId)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
sendCompletionToClient(fullText, ctx.originalRequest, true);
|
||||
QString fullText;
|
||||
if (Session *session = it.value().session) {
|
||||
if (auto *history = session->history(); history && !history->isEmpty())
|
||||
fullText = history->messages().back().text();
|
||||
}
|
||||
const QJsonObject originalRequest = it.value().originalRequest;
|
||||
|
||||
m_activeRequests.erase(it);
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
sendCompletionToClient(fullText, originalRequest, true);
|
||||
finishRequest(requestId);
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId) || !info.usage)
|
||||
return;
|
||||
|
||||
const auto &u = *info.usage;
|
||||
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||
.arg(requestId)
|
||||
.arg(u.promptTokens)
|
||||
.arg(u.completionTokens)
|
||||
.arg(u.cachedPromptTokens)
|
||||
.arg(u.reasoningTokens));
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
|
||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
||||
|
||||
// Send LSP error response to client
|
||||
QJsonObject response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
|
||||
response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
|
||||
|
||||
QJsonObject errorObject;
|
||||
errorObject["code"] = -32603; // Internal error code
|
||||
errorObject["message"] = error;
|
||||
response["error"] = errorObject;
|
||||
|
||||
|
||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||
|
||||
finishRequest(requestId);
|
||||
}
|
||||
|
||||
void LLMClientInterface::finishRequest(const QString &requestId)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
Session *session = it.value().session;
|
||||
m_activeRequests.erase(it);
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
|
||||
if (session)
|
||||
m_sessionManager.release(session);
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendData(const QByteArray &data)
|
||||
@@ -134,26 +154,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
|
||||
void LLMClientInterface::handleCancelRequest()
|
||||
{
|
||||
QSet<PluginLLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider->client(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
const RequestContext &ctx = it.value();
|
||||
if (ctx.provider) {
|
||||
ctx.provider->cancelRequest(it.key());
|
||||
}
|
||||
}
|
||||
|
||||
const auto requests = m_activeRequests;
|
||||
m_activeRequests.clear();
|
||||
|
||||
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||
m_performanceLogger.endTimeMeasurement(it.key());
|
||||
if (Session *session = it.value().session)
|
||||
m_sessionManager.release(session);
|
||||
}
|
||||
|
||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||
}
|
||||
|
||||
@@ -192,34 +201,19 @@ void LLMClientInterface::handleShutdown(const QJsonObject &request)
|
||||
|
||||
void LLMClientInterface::handleTextDocumentDidOpen(const QJsonObject &request) {}
|
||||
|
||||
void LLMClientInterface::handleInitialized(const QJsonObject &request)
|
||||
{
|
||||
QJsonObject response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response["method"] = "initialized";
|
||||
response["params"] = QJsonObject();
|
||||
|
||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleExit(const QJsonObject &request)
|
||||
{
|
||||
emit finished();
|
||||
}
|
||||
|
||||
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
|
||||
{
|
||||
QJsonObject response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response[LanguageServerProtocol::idKey] = request["id"];
|
||||
|
||||
|
||||
QJsonObject errorObject;
|
||||
errorObject["code"] = -32603; // Internal error code
|
||||
errorObject["message"] = errorMessage;
|
||||
response["error"] = errorObject;
|
||||
|
||||
|
||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||
|
||||
|
||||
// End performance measurement if it was started
|
||||
QString requestId = request["id"].toString();
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
@@ -236,130 +230,103 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
return;
|
||||
}
|
||||
|
||||
auto updatedContext = prepareContext(request, documentInfo);
|
||||
|
||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||
|
||||
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
||||
: m_generalSettings.ccPreset1Provider();
|
||||
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
|
||||
: m_generalSettings.ccPreset1Model();
|
||||
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
|
||||
: m_generalSettings.ccPreset1Url();
|
||||
|
||||
const auto provider = m_providerRegistry.getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
||||
const QString agentName = pickCompletionAgent(filePath);
|
||||
if (agentName.isEmpty()) {
|
||||
QString error = QString("No code completion agent matches: %1").arg(filePath);
|
||||
LOG_MESSAGE(error);
|
||||
sendErrorResponse(request, error);
|
||||
return;
|
||||
}
|
||||
|
||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||
: m_generalSettings.ccPreset1Template();
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager.acquire(agentName, &sessionError);
|
||||
if (!session) {
|
||||
LOG_MESSAGE(sessionError);
|
||||
sendErrorResponse(request, sessionError);
|
||||
return;
|
||||
}
|
||||
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
Templates::ContextRenderer::Bindings bindings;
|
||||
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||
Utils::FilePath::fromString(filePath)))
|
||||
bindings.projectDir = project->projectDirectory().toFSPathString();
|
||||
bindings.configDir = AgentFactory::userConfigDir();
|
||||
bindings.language = CodeHandler::detectLanguageFromExtension(QFileInfo(filePath).suffix());
|
||||
session->setContextBindings(bindings);
|
||||
|
||||
if (!promptTemplate) {
|
||||
QString error = QString("No template found with name: %1").arg(templateName);
|
||||
Templates::ContextData context = prepareContext(request, documentInfo);
|
||||
|
||||
QString editorContext;
|
||||
if (context.fileContext.has_value())
|
||||
editorContext.append(context.fileContext.value());
|
||||
|
||||
if (m_completeSettings.useOpenFilesContext())
|
||||
editorContext.append(m_contextManager->openedFilesContext({filePath}));
|
||||
|
||||
if (!editorContext.isEmpty())
|
||||
session->systemPrompt()->setLayer(QStringLiteral("completion.context"), editorContext);
|
||||
|
||||
connect(
|
||||
session,
|
||||
&Session::finished,
|
||||
this,
|
||||
[this, session](const LLMQore::RequestID &, const QString &) {
|
||||
onCompletionFinished(requestIdForSession(session));
|
||||
});
|
||||
connect(
|
||||
session,
|
||||
&Session::failed,
|
||||
this,
|
||||
[this, session](const LLMQore::RequestID &, const QodeAssist::ErrorInfo &error) {
|
||||
onCompletionFailed(requestIdForSession(session), error.message);
|
||||
});
|
||||
|
||||
if (auto *client = session->client())
|
||||
client->setTransferTimeout(static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
||||
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
blocks.push_back(
|
||||
std::make_unique<CompletionContent>(
|
||||
context.prefix.value_or(QString()), context.suffix.value_or(QString())));
|
||||
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
||||
if (requestId.isEmpty()) {
|
||||
QString error = QString("Failed to start completion request for agent '%1': %2")
|
||||
.arg(agentName, session->lastError().message);
|
||||
m_sessionManager.removeSession(session);
|
||||
LOG_MESSAGE(error);
|
||||
sendErrorResponse(request, error);
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject payload{{"model", modelName}, {"stream", true}};
|
||||
|
||||
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
|
||||
if (!stopWords.isEmpty())
|
||||
payload["stop"] = stopWords;
|
||||
|
||||
QString systemPrompt;
|
||||
if (m_completeSettings.useSystemPrompt())
|
||||
systemPrompt.append(
|
||||
m_completeSettings.useUserMessageTemplateForCC()
|
||||
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
|
||||
? m_completeSettings.systemPromptForNonFimModels()
|
||||
: m_completeSettings.systemPrompt());
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules
|
||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
LOG_MESSAGE("Loaded project rules for completion");
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedContext.fileContext.has_value())
|
||||
systemPrompt.append(updatedContext.fileContext.value());
|
||||
|
||||
if (m_completeSettings.useOpenFilesContext()) {
|
||||
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
|
||||
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
||||
if (!updatedContext.filesMetadata) {
|
||||
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
|
||||
}
|
||||
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
||||
}
|
||||
} else {
|
||||
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
|
||||
}
|
||||
}
|
||||
|
||||
updatedContext.systemPrompt = systemPrompt;
|
||||
|
||||
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
|
||||
QString userMessage;
|
||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
||||
userMessage = m_completeSettings.processMessageToFIM(
|
||||
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
|
||||
} else {
|
||||
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
|
||||
}
|
||||
|
||||
// TODO refactor add message
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
messages.append({"user", userMessage});
|
||||
updatedContext.history = messages;
|
||||
}
|
||||
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
updatedContext,
|
||||
PluginLLMCore::RequestType::CodeCompletion,
|
||||
false,
|
||||
false);
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&LLMClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&LLMClientInterface::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&LLMClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
m_activeRequests[requestId] = {request, session};
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
|
||||
{
|
||||
const QStringList roster = Settings::PipelinesConfig::loadCached().rosters.codeCompletion;
|
||||
if (roster.isEmpty())
|
||||
return {};
|
||||
|
||||
AgentRouter::Context ctx;
|
||||
ctx.filePath = filePath;
|
||||
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||
Utils::FilePath::fromString(filePath)))
|
||||
ctx.projectName = project->displayName();
|
||||
|
||||
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
|
||||
}
|
||||
|
||||
QString LLMClientInterface::requestIdForSession(Session *session) const
|
||||
{
|
||||
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||
if (it.value().session == session)
|
||||
return it.key();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Templates::ContextData LLMClientInterface::prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||
{
|
||||
QJsonObject params = request["params"].toObject();
|
||||
@@ -373,14 +340,6 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
|
||||
QString LLMClientInterface::resolveEndpoint(
|
||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
|
||||
{
|
||||
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||
: m_generalSettings.ccCustomEndpoint();
|
||||
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
|
||||
}
|
||||
|
||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
{
|
||||
return m_contextManager;
|
||||
@@ -389,15 +348,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
void LLMClientInterface::sendCompletionToClient(
|
||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||
{
|
||||
auto filePath = Context::extractFilePathFromRequest(request);
|
||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||
|
||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||
: m_generalSettings.ccPreset1Template();
|
||||
|
||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||
|
||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||
|
||||
QJsonObject response;
|
||||
@@ -416,14 +366,13 @@ void LLMClientInterface::sendCompletionToClient(
|
||||
if (outputHandler == "Raw text") {
|
||||
processedCompletion = completion;
|
||||
} else if (outputHandler == "Force processing") {
|
||||
processedCompletion = CodeHandler::processText(completion,
|
||||
Context::extractFilePathFromRequest(request));
|
||||
processedCompletion
|
||||
= CodeHandler::processText(completion, Context::extractFilePathFromRequest(request));
|
||||
} else { // "Auto"
|
||||
processedCompletion = CodeHandler::hasCodeBlocks(completion)
|
||||
? CodeHandler::processText(completion,
|
||||
Context::extractFilePathFromRequest(
|
||||
request))
|
||||
: completion;
|
||||
processedCompletion
|
||||
= CodeHandler::hasCodeBlocks(completion)
|
||||
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
|
||||
: completion;
|
||||
}
|
||||
|
||||
if (processedCompletion.endsWith('\n')) {
|
||||
@@ -435,11 +384,11 @@ void LLMClientInterface::sendCompletionToClient(
|
||||
}
|
||||
|
||||
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
|
||||
|
||||
|
||||
QJsonObject range;
|
||||
range["start"] = position;
|
||||
range["end"] = position;
|
||||
|
||||
|
||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||
completions.append(completionItem);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -7,12 +8,11 @@
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <QPointer>
|
||||
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <context/ProgrammingLanguage.hpp>
|
||||
#include <pluginllmcore/ContextData.hpp>
|
||||
#include <pluginllmcore/IPromptProvider.hpp>
|
||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||
#include <logger/IRequestPerformanceLogger.hpp>
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
@@ -22,6 +22,14 @@ class QNetworkAccessManager;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class AgentFactory;
|
||||
class Session;
|
||||
class SessionManager;
|
||||
|
||||
namespace Templates {
|
||||
struct ContextData;
|
||||
}
|
||||
|
||||
class LLMClientInterface : public LanguageClient::BaseClientInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -30,8 +38,8 @@ public:
|
||||
LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||
PluginLLMCore::IPromptProvider *promptProvider,
|
||||
AgentFactory &agentFactory,
|
||||
SessionManager &sessionManager,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger);
|
||||
~LLMClientInterface() override;
|
||||
@@ -51,37 +59,33 @@ public:
|
||||
protected:
|
||||
void startImpl() override;
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
void handleInitialize(const QJsonObject &request);
|
||||
void handleShutdown(const QJsonObject &request);
|
||||
void handleTextDocumentDidOpen(const QJsonObject &request);
|
||||
void handleInitialized(const QJsonObject &request);
|
||||
void handleExit(const QJsonObject &request);
|
||||
void handleCancelRequest();
|
||||
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
||||
|
||||
void onCompletionFinished(const QString &requestId);
|
||||
void onCompletionFailed(const QString &requestId, const QString &error);
|
||||
void finishRequest(const QString &requestId);
|
||||
QString requestIdForSession(Session *session) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
PluginLLMCore::Provider *provider;
|
||||
QPointer<Session> session;
|
||||
};
|
||||
|
||||
PluginLLMCore::ContextData prepareContext(
|
||||
Templates::ContextData prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||
|
||||
QString resolveEndpoint(
|
||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
|
||||
QString pickCompletionAgent(const QString &filePath) const;
|
||||
|
||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||
const Settings::GeneralSettings &m_generalSettings;
|
||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
||||
AgentFactory &m_agentFactory;
|
||||
SessionManager &m_sessionManager;
|
||||
Context::IDocumentReader &m_documentReader;
|
||||
IRequestPerformanceLogger &m_performanceLogger;
|
||||
QElapsedTimer m_completionTimer;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
@@ -33,7 +35,6 @@ namespace QodeAssist {
|
||||
class Completion : public LanguageServerProtocol::JsonObject
|
||||
{
|
||||
static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"};
|
||||
static constexpr LanguageServerProtocol::Key uuidKey{"uuid"};
|
||||
|
||||
public:
|
||||
using JsonObject::JsonObject;
|
||||
@@ -53,7 +54,6 @@ public:
|
||||
}
|
||||
QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); }
|
||||
void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); }
|
||||
QString uuid() const { return typedValue<QString>(uuidKey); }
|
||||
|
||||
bool isValid() const override
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.9.19",
|
||||
"Version" : "0.9.21",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||
"License" : "GPLv3",
|
||||
"License" : "GPLv3 with additional attribution terms (§7b) — see LICENSE",
|
||||
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
*/
|
||||
|
||||
#include "QodeAssistClient.hpp"
|
||||
@@ -157,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||
}
|
||||
|
||||
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
|
||||
{
|
||||
m_agentFactory = agentFactory;
|
||||
}
|
||||
|
||||
QodeAssistClient::~QodeAssistClient()
|
||||
{
|
||||
cleanupConnections();
|
||||
@@ -261,9 +273,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
||||
return;
|
||||
|
||||
|
||||
if (m_llmClient->contextManager()
|
||||
->ignoreManager()
|
||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
||||
if (m_llmClient->contextManager()->shouldIgnore(
|
||||
editor->textDocument()->filePath().toUrlishString())) {
|
||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||
return;
|
||||
@@ -307,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
|
||||
if (m_llmClient->contextManager()
|
||||
->ignoreManager()
|
||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
||||
if (m_llmClient->contextManager()->shouldIgnore(
|
||||
editor->textDocument()->filePath().toUrlishString())) {
|
||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||
return;
|
||||
@@ -317,6 +327,8 @@ void QodeAssistClient::requestQuickRefactor(
|
||||
|
||||
if (!m_refactorHandler) {
|
||||
m_refactorHandler = new QuickRefactorHandler(this);
|
||||
m_refactorHandler->setSessionManager(m_sessionManager);
|
||||
m_refactorHandler->setAgentFactory(m_agentFactory);
|
||||
connect(
|
||||
m_refactorHandler,
|
||||
&QuickRefactorHandler::refactoringCompleted,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
#include "LSPCompletion.hpp"
|
||||
@@ -12,14 +14,14 @@
|
||||
#include "RefactorSuggestionHoverHandler.hpp"
|
||||
#include "widgets/CompletionProgressHandler.hpp"
|
||||
#include "widgets/CompletionErrorHandler.hpp"
|
||||
#include "widgets/EditorChatButtonHandler.hpp"
|
||||
#include "widgets/RefactorWidgetHandler.hpp"
|
||||
#include <languageclient/client.h>
|
||||
#include <pluginllmcore/IPromptProvider.hpp>
|
||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class SessionManager;
|
||||
class AgentFactory;
|
||||
|
||||
class QodeAssistClient : public LanguageClient::Client
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -27,6 +29,9 @@ public:
|
||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||
~QodeAssistClient() override;
|
||||
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setAgentFactory(AgentFactory *agentFactory);
|
||||
|
||||
void openDocument(TextEditor::TextDocument *document) override;
|
||||
bool canOpenProject(ProjectExplorer::Project *project) override;
|
||||
|
||||
@@ -62,11 +67,12 @@ private:
|
||||
int m_recentCharCount;
|
||||
CompletionProgressHandler m_progressHandler;
|
||||
CompletionErrorHandler m_errorHandler;
|
||||
EditorChatButtonHandler m_chatButtonHandler;
|
||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||
LLMClientInterface *m_llmClient;
|
||||
SessionManager *m_sessionManager{nullptr};
|
||||
AgentFactory *m_agentFactory{nullptr};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "QuickRefactorHandler.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <LLMQore/ContentBlocks.hpp>
|
||||
#include <LLMQore/ToolLoopRunner.hpp>
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <utils/filepath.h>
|
||||
|
||||
#include <context/DocumentContextReader.hpp>
|
||||
#include <pluginllmcore/ResponseCleaner.hpp>
|
||||
#include <context/DocumentReaderQtCreator.hpp>
|
||||
#include <context/EnvBlockFormatter.hpp>
|
||||
#include <context/Utils.hpp>
|
||||
#include <pluginllmcore/PromptTemplateManager.hpp>
|
||||
#include <pluginllmcore/ProvidersManager.hpp>
|
||||
#include <pluginllmcore/RulesLoader.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/ChatAssistantSettings.hpp>
|
||||
#include <sources/common/ResponseCleaner.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/QuickRefactorSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
|
||||
#include "sources/common/ContextData.hpp"
|
||||
|
||||
#include <AgentConfig.hpp>
|
||||
#include <AgentFactory.hpp>
|
||||
#include <ContextRenderer.hpp>
|
||||
#include <ConversationHistory.hpp>
|
||||
#include <Session.hpp>
|
||||
#include <SessionManager.hpp>
|
||||
#include <SystemPromptBuilder.hpp>
|
||||
|
||||
#include "sources/settings/PipelinesConfig.hpp"
|
||||
#include "tools/ToolsRegistration.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
||||
@@ -33,6 +53,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
||||
|
||||
QuickRefactorHandler::~QuickRefactorHandler() {}
|
||||
|
||||
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
|
||||
{
|
||||
m_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
|
||||
{
|
||||
m_agentFactory = agentFactory;
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::sendRefactorRequest(
|
||||
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
||||
{
|
||||
@@ -87,102 +117,114 @@ void QuickRefactorHandler::sendRefactorRequest(
|
||||
prepareAndSendRequest(editor, instructions, range);
|
||||
}
|
||||
|
||||
QString QuickRefactorHandler::configuredAgent(AgentFactory *agentFactory)
|
||||
{
|
||||
const QString configured = Settings::PipelinesConfig::load().rosters.quickRefactor;
|
||||
if (configured.isEmpty() || !agentFactory || !agentFactory->configByName(configured))
|
||||
return {};
|
||||
return configured;
|
||||
}
|
||||
|
||||
QString QuickRefactorHandler::pickRefactorAgent() const
|
||||
{
|
||||
return configuredAgent(m_agentFactory);
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::prepareAndSendRequest(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const QString &instructions,
|
||||
const Utils::Text::Range &range)
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
|
||||
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
|
||||
|
||||
const auto providerName = settings.qrProvider();
|
||||
auto provider = providerRegistry.getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
||||
const auto emitError = [this, editor](const QString &error) {
|
||||
LOG_MESSAGE(error);
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
result.editor = editor;
|
||||
emit refactoringCompleted(result);
|
||||
};
|
||||
|
||||
if (!m_sessionManager) {
|
||||
emitError(QStringLiteral("Quick refactor session manager is not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto templateName = settings.qrTemplate();
|
||||
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
QString error = QString("No template found with name: %1").arg(templateName);
|
||||
LOG_MESSAGE(error);
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
result.editor = editor;
|
||||
emit refactoringCompleted(result);
|
||||
const QString agentName = pickRefactorAgent();
|
||||
if (agentName.isEmpty()) {
|
||||
emitError(QStringLiteral(
|
||||
"No quick refactor agent configured. Set one in QodeAssist > General."));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
||||
QString sessionError;
|
||||
Session *session = m_sessionManager->acquire(agentName, &sessionError);
|
||||
if (!session) {
|
||||
emitError(sessionError.isEmpty() ? QStringLiteral("No quick refactor agent selected")
|
||||
: sessionError);
|
||||
return;
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||
auto *client = session->client();
|
||||
if (!client) {
|
||||
m_sessionManager->removeSession(session);
|
||||
emitError(QStringLiteral("Quick refactor agent has no live client"));
|
||||
return;
|
||||
}
|
||||
|
||||
bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
context,
|
||||
PluginLLMCore::RequestType::QuickRefactoring,
|
||||
enableTools,
|
||||
enableThinking);
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
Templates::ContextRenderer::Bindings bindings;
|
||||
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
||||
bindings.configDir = AgentFactory::userConfigDir();
|
||||
session->setContextBindings(bindings);
|
||||
|
||||
provider->client()->setMaxToolContinuations(
|
||||
Settings::toolsSettings().maxToolContinuations());
|
||||
const AgentConfig *agentConfig
|
||||
= m_agentFactory ? m_agentFactory->configByName(agentName) : nullptr;
|
||||
if (agentConfig && agentConfig->enableTools) {
|
||||
m_sessionManager->toolContributors().contribute(client->tools());
|
||||
client->toolLoop()->setMaxRounds(Settings::toolsSettings().maxToolContinuations());
|
||||
}
|
||||
|
||||
session->systemPrompt()->setLayer(
|
||||
QStringLiteral("refactor"), buildContextLayer(editor, range));
|
||||
|
||||
client->setTransferTimeout(
|
||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||
|
||||
m_isRefactoringInProgress = true;
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&QuickRefactorHandler::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
session, &Session::finished, this,
|
||||
[this](const LLMQore::RequestID &id, const QString &) { onRefactorFinished(id); });
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&QuickRefactorHandler::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
session, &Session::failed, this,
|
||||
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||
onRefactorFailed(id, error);
|
||||
});
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&QuickRefactorHandler::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||
const QString userMessage = instructions.isEmpty()
|
||||
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
|
||||
: instructions;
|
||||
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
|
||||
|
||||
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
||||
if (requestId.isEmpty()) {
|
||||
m_isRefactoringInProgress = false;
|
||||
const QString reason = session->lastError().message;
|
||||
m_sessionManager->removeSession(session);
|
||||
emitError(QStringLiteral("Failed to start quick refactor request for agent '%1': %2")
|
||||
.arg(agentName, reason));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
||||
m_lastRequestId = requestId;
|
||||
QJsonObject request{{"id", requestId}};
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
||||
}
|
||||
|
||||
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &instructions)
|
||||
QString QuickRefactorHandler::buildContextLayer(
|
||||
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
|
||||
{
|
||||
PluginLLMCore::ContextData context;
|
||||
Q_UNUSED(range)
|
||||
|
||||
auto textDocument = editor->textDocument();
|
||||
Context::DocumentReaderQtCreator documentReader;
|
||||
@@ -190,7 +232,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
|
||||
if (!documentInfo.document) {
|
||||
LOG_MESSAGE("Error: Document is not available");
|
||||
return context;
|
||||
return {};
|
||||
}
|
||||
|
||||
QTextCursor cursor = editor->textCursor();
|
||||
@@ -264,43 +306,20 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
taggedContent = contextBefore + "<cursor>" + contextAfter;
|
||||
}
|
||||
|
||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
||||
QString contextLayer = Context::EnvBlockFormatter::formatFile(
|
||||
{documentInfo.filePath, documentInfo.mimeType});
|
||||
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
||||
project, PluginLLMCore::RulesContext::QuickRefactor);
|
||||
contextLayer += "\n# Code Context with Position Markers\n" + taggedContent;
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
LOG_MESSAGE("Loaded project rules for quick refactor");
|
||||
}
|
||||
}
|
||||
|
||||
systemPrompt += "\n\nFile information:";
|
||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||
|
||||
systemPrompt += "\n\n# Code Context with Position Markers\n" + taggedContent;
|
||||
|
||||
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
|
||||
systemPrompt += cursor.hasSelection()
|
||||
contextLayer += "\n\n# What to Generate:";
|
||||
contextLayer += cursor.hasSelection()
|
||||
? "\n- Generate ONLY the code that should REPLACE the selected text between "
|
||||
"<selection_start> and <selection_end> markers"
|
||||
"\n- Your output will completely replace the selected code"
|
||||
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||
"\n- Your output will be inserted at the cursor location";
|
||||
|
||||
systemPrompt += "\n\n## Formatting Rules:"
|
||||
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
||||
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
||||
"\n- Do NOT add comments explaining what you changed"
|
||||
"\n- Do NOT repeat existing code, be precise with context"
|
||||
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
||||
"\n- The output must be ready to insert directly into the editor as-is";
|
||||
|
||||
systemPrompt += "\n\n## Indentation and Whitespace:";
|
||||
|
||||
|
||||
QString indentNote;
|
||||
if (cursor.hasSelection()) {
|
||||
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||
int leadingSpaces = 0;
|
||||
@@ -310,12 +329,12 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
else break;
|
||||
}
|
||||
if (leadingSpaces > 0) {
|
||||
systemPrompt += QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
||||
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
||||
"\n- Each line in your output must maintain this base indentation")
|
||||
.arg(leadingSpaces);
|
||||
indentNote = QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
||||
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
||||
"\n- Each line in your output must maintain this base indentation")
|
||||
.arg(leadingSpaces);
|
||||
}
|
||||
systemPrompt += "\n- PRESERVE all indentation from the original code";
|
||||
indentNote += "\n- PRESERVE all indentation from the original code";
|
||||
} else {
|
||||
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
||||
QString lineText = block.text();
|
||||
@@ -326,61 +345,20 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
else break;
|
||||
}
|
||||
if (leadingSpaces > 0) {
|
||||
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation"
|
||||
"\n- If generating multiline code, EVERY line must start with at least %1 spaces"
|
||||
"\n- If generating single-line code, it will be inserted inline (no indentation needed)")
|
||||
.arg(leadingSpaces);
|
||||
indentNote = QString("\n- CRITICAL: Current line has %1 spaces of indentation"
|
||||
"\n- If generating multiline code, EVERY line must start with at least %1 spaces"
|
||||
"\n- If generating single-line code, it will be inserted inline (no indentation needed)")
|
||||
.arg(leadingSpaces);
|
||||
}
|
||||
}
|
||||
|
||||
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
||||
"\n- Maintain consistent indentation for nested blocks"
|
||||
"\n- Do NOT remove or reduce the base indentation level"
|
||||
"\n\n## Code Style:"
|
||||
"\n- Match the coding style of the surrounding code (naming, spacing, braces, etc.)"
|
||||
"\n- Preserve the original code structure when possible"
|
||||
"\n- Only change what is necessary to fulfill the user's request";
|
||||
if (!indentNote.isEmpty())
|
||||
contextLayer += "\n\n## Indentation:" + indentNote;
|
||||
|
||||
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
|
||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||
contextLayer += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||
}
|
||||
|
||||
context.systemPrompt = systemPrompt;
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
messages.append(
|
||||
{"user",
|
||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
||||
: instructions});
|
||||
context.history = messages;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleLLMResponse(
|
||||
const QString &response, const QJsonObject &request, bool isComplete)
|
||||
{
|
||||
if (request["id"].toString() != m_lastRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
m_isRefactoringInProgress = false;
|
||||
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
|
||||
|
||||
RefactorResult result;
|
||||
result.newText = cleanedResponse;
|
||||
result.insertRange = m_currentRange;
|
||||
result.success = true;
|
||||
result.editor = m_currentEditor;
|
||||
|
||||
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
||||
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
||||
LOG_MESSAGE(cleanedResponse);
|
||||
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
||||
|
||||
emit refactoringCompleted(result);
|
||||
}
|
||||
return contextLayer;
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::cancelRequest()
|
||||
@@ -394,10 +372,10 @@ void QuickRefactorHandler::cancelRequest()
|
||||
|
||||
auto it = m_activeRequests.find(id);
|
||||
if (it != m_activeRequests.end()) {
|
||||
auto provider = it.value().provider;
|
||||
Session *session = it.value().session;
|
||||
m_activeRequests.erase(it);
|
||||
if (provider)
|
||||
provider->cancelRequest(id);
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->release(session);
|
||||
}
|
||||
|
||||
RefactorResult result;
|
||||
@@ -406,42 +384,66 @@ void QuickRefactorHandler::cancelRequest()
|
||||
emit refactoringCompleted(result);
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||
void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
m_activeRequests.remove(requestId);
|
||||
QJsonObject request{{"id", requestId}};
|
||||
handleLLMResponse(fullText, request, true);
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (requestId != m_lastRequestId || !info.usage)
|
||||
if (requestId != m_lastRequestId)
|
||||
return;
|
||||
|
||||
const auto &u = *info.usage;
|
||||
LOG_MESSAGE(
|
||||
QString("Quick refactor usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||
.arg(requestId)
|
||||
.arg(u.promptTokens)
|
||||
.arg(u.completionTokens)
|
||||
.arg(u.cachedPromptTokens)
|
||||
.arg(u.reasoningTokens));
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||
if (it != m_activeRequests.end())
|
||||
m_activeRequests.erase(it);
|
||||
|
||||
QString fullText;
|
||||
if (session) {
|
||||
if (auto *history = session->history(); history && !history->isEmpty())
|
||||
fullText = history->messages().back().text();
|
||||
}
|
||||
|
||||
m_isRefactoringInProgress = false;
|
||||
m_lastRequestId.clear();
|
||||
|
||||
const QString cleanedResponse = ResponseCleaner::clean(fullText);
|
||||
|
||||
RefactorResult result;
|
||||
result.newText = cleanedResponse;
|
||||
result.insertRange = m_currentRange;
|
||||
result.success = true;
|
||||
result.editor = m_currentEditor;
|
||||
|
||||
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
||||
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
||||
LOG_MESSAGE(cleanedResponse);
|
||||
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
||||
|
||||
emit refactoringCompleted(result);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->release(session);
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
void QuickRefactorHandler::onRefactorFailed(
|
||||
const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
m_activeRequests.remove(requestId);
|
||||
m_isRefactoringInProgress = false;
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error;
|
||||
result.editor = m_currentEditor;
|
||||
emit refactoringCompleted(result);
|
||||
}
|
||||
if (requestId != m_lastRequestId)
|
||||
return;
|
||||
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||
if (it != m_activeRequests.end())
|
||||
m_activeRequests.erase(it);
|
||||
|
||||
m_isRefactoringInProgress = false;
|
||||
m_lastRequestId.clear();
|
||||
|
||||
RefactorResult result;
|
||||
result.success = false;
|
||||
result.errorMessage = error.message;
|
||||
result.editor = m_currentEditor;
|
||||
emit refactoringCompleted(result);
|
||||
|
||||
if (session && m_sessionManager)
|
||||
m_sessionManager->release(session);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/textutils.h>
|
||||
|
||||
#include <ErrorInfo.hpp>
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <pluginllmcore/ContextData.hpp>
|
||||
#include <pluginllmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
class SessionManager;
|
||||
class Session;
|
||||
class AgentFactory;
|
||||
|
||||
struct RefactorResult
|
||||
{
|
||||
QString newText;
|
||||
Utils::Text::Range insertRange;
|
||||
bool success;
|
||||
QString errorMessage;
|
||||
TextEditor::TextEditorWidget *editor{nullptr};
|
||||
QPointer<TextEditor::TextEditorWidget> editor;
|
||||
};
|
||||
|
||||
class QuickRefactorHandler : public QObject
|
||||
@@ -34,40 +39,41 @@ public:
|
||||
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
||||
~QuickRefactorHandler() override;
|
||||
|
||||
void setSessionManager(SessionManager *sessionManager);
|
||||
void setAgentFactory(AgentFactory *agentFactory);
|
||||
|
||||
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
||||
|
||||
void cancelRequest();
|
||||
bool isProcessing() const { return m_isRefactoringInProgress; }
|
||||
|
||||
static QString configuredAgent(AgentFactory *agentFactory);
|
||||
|
||||
signals:
|
||||
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
void prepareAndSendRequest(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const QString &instructions,
|
||||
const Utils::Text::Range &range);
|
||||
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||
PluginLLMCore::ContextData prepareContext(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &instructions);
|
||||
void onRefactorFinished(const QString &requestId);
|
||||
void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||
QString buildContextLayer(
|
||||
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
|
||||
QString pickRefactorAgent() const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
PluginLLMCore::Provider *provider;
|
||||
QPointer<Session> session;
|
||||
};
|
||||
|
||||
QPointer<SessionManager> m_sessionManager;
|
||||
QPointer<AgentFactory> m_agentFactory;
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
TextEditor::TextEditorWidget *m_currentEditor;
|
||||
QPointer<TextEditor::TextEditorWidget> m_currentEditor;
|
||||
Utils::Text::Range m_currentRange;
|
||||
bool m_isRefactoringInProgress;
|
||||
QString m_lastRequestId;
|
||||
|
||||
159
README.md
159
README.md
@@ -41,7 +41,7 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
||||
- **File Context** — attach, link, or auto-sync open editor files for richer prompts
|
||||
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint
|
||||
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
|
||||
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
|
||||
- **Customizable** — per-agent personas (agent TOML `system_prompt`), reusable refactor templates, full prompt-template control
|
||||
|
||||
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
||||
|
||||
@@ -99,7 +99,8 @@ You can install and update QodeAssist directly from within Qt Creator by adding
|
||||
1. Open the Extensions page (`Qt Creator → Extensions`) and switch to the **Browser** tab
|
||||
2. Enable **Use External Repository**
|
||||
3. Next to **Repository URLs**, click **Add** and paste the registry archive URL matching your Qt Creator version:
|
||||
- **Latest (QtC 19)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
|
||||
- **Latest (up to date to QtCreator release)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
|
||||
- **QtC 20**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc20.tar.gz`
|
||||
- **QtC 19**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc19.tar.gz`
|
||||
- **QtC 18**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc18.tar.gz`
|
||||
<details>
|
||||
@@ -216,9 +217,8 @@ For optimal coding assistance, we recommend using these top-tier models:
|
||||
|
||||
### Additional Configuration
|
||||
|
||||
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
||||
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents (including personas via `system_prompt`) with TOML profiles
|
||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
||||
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
||||
|
||||
## Features
|
||||
@@ -254,7 +254,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
||||
- Multiple chat panels: side panel, bottom panel, and popup window
|
||||
- Chat history with auto-save and restore
|
||||
- Token usage monitoring
|
||||
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
|
||||
- AI personas via agent `system_prompt` — switch personas by switching agents (see [Creating Agents](docs/creating-agents.md))
|
||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
||||
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
||||
- Automatic syncing with open editor files (optional)
|
||||
@@ -348,18 +348,16 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │
|
||||
│ │
|
||||
│ 1. System Prompt (from Code Completion Settings - FIM variant) │
|
||||
│ 2. Project Rules: │
|
||||
│ └─ .qodeassist/rules/completion/*.md │
|
||||
│ 3. Open Files Context (optional, if enabled): │
|
||||
│ └─ Currently open editor files │
|
||||
│ 4. Code Context: │
|
||||
│ 1. Editor Context: │
|
||||
│ ├─ File information (language, path) │
|
||||
│ ├─ Recent project changes (optional, if enabled) │
|
||||
│ └─ Open editor files (optional, if enabled) │
|
||||
│ 2. Code Context: │
|
||||
│ ├─ Code before cursor (prefix) │
|
||||
│ └─ Code after cursor (suffix) │
|
||||
│ │
|
||||
│ Final Prompt: FIM_Template(Prefix: SystemPrompt + Rules + OpenFiles + │
|
||||
│ CodeBefore, │
|
||||
│ Suffix: CodeAfter) │
|
||||
│ Final Request: the agent's TOML [body] template renders prefix/suffix │
|
||||
│ into the provider's native FIM fields │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -374,21 +372,19 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │
|
||||
│ │
|
||||
│ 1. System Prompt (from Code Completion Settings - Non-FIM variant) │
|
||||
│ └─ Includes response formatting instructions │
|
||||
│ 2. Project Rules: │
|
||||
│ └─ .qodeassist/rules/completion/*.md │
|
||||
│ 3. Open Files Context (optional, if enabled): │
|
||||
│ └─ Currently open editor files │
|
||||
│ 4. Code Context: │
|
||||
│ 1. Completion Instructions (from the agent's TOML profile) │
|
||||
│ └─ Includes response formatting rules │
|
||||
│ 2. Editor Context: │
|
||||
│ ├─ File information (language, path) │
|
||||
│ ├─ Recent project changes (optional, if enabled) │
|
||||
│ └─ Open editor files (optional, if enabled) │
|
||||
│ 3. Code Context: │
|
||||
│ ├─ Code before cursor │
|
||||
│ ├─ <cursor> marker │
|
||||
│ └─ Code after cursor │
|
||||
│ 5. User Message: "Complete the code at cursor position" │
|
||||
│ │
|
||||
│ Final Prompt: [System: SystemPrompt + Rules] │
|
||||
│ [User: OpenFiles + Context + CompletionRequest] │
|
||||
│ Final Prompt: [System: Instructions + EditorContext] │
|
||||
│ [User: Code around cursor as a completion request] │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -401,27 +397,22 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ CHAT ASSISTANT │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 1. System Prompt (from Chat Assistant Settings) │
|
||||
│ 2. Agent Role (optional, from role selector): │
|
||||
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
|
||||
│ 3. Project Rules: │
|
||||
│ ├─ .qodeassist/rules/common/*.md │
|
||||
│ └─ .qodeassist/rules/chat/*.md │
|
||||
│ 4. File Context (optional): │
|
||||
│ ├─ Attached files (manual) │
|
||||
│ ├─ Linked files (persistent) │
|
||||
│ └─ Open editor files (if auto-sync enabled) │
|
||||
│ 5. Tool Definitions (if enabled): │
|
||||
│ ├─ ReadProjectFileByName │
|
||||
│ ├─ ListProjectFiles │
|
||||
│ ├─ SearchInProject │
|
||||
│ └─ GetIssuesList │
|
||||
│ 6. Conversation History │
|
||||
│ 7. User Message │
|
||||
│ 1. Agent System Prompt (persona, from the agent's TOML profile) │
|
||||
│ 2. Project Info + Skills (catalog and always-on skills) │
|
||||
│ 3. Tool Definitions (if the agent enables tools) │
|
||||
│ 4. Conversation History: │
|
||||
│ ├─ Previous messages and tool calls/results │
|
||||
│ ├─ Attachments stay with the message they were sent with │
|
||||
│ └─ /skill instructions persist for the whole conversation │
|
||||
│ 5. Linked Files + Open Editor Files (if auto-sync enabled): │
|
||||
│ └─ FRESH snapshot of current file content, re-read on every │
|
||||
│ request and placed next to your latest message — never │
|
||||
│ duplicated into the history │
|
||||
│ 6. User Message (+ this turn's attachments and images) │
|
||||
│ │
|
||||
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
|
||||
│ Final Prompt: [System: Persona + ProjectInfo + Skills] │
|
||||
│ [History: Previous messages] │
|
||||
│ [User: FileContext + UserMessage] │
|
||||
│ [User: CurrentFilesSnapshot + UserMessage + Attachments] │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -434,28 +425,26 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ QUICK REFACTORING │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ 1. System Prompt (from Quick Refactor Settings) │
|
||||
│ 2. Project Rules: │
|
||||
│ ├─ .qodeassist/rules/common/*.md │
|
||||
│ └─ .qodeassist/rules/quickrefactor/*.md │
|
||||
│ 3. Code Context: │
|
||||
│ 1. Agent System Prompt (persona, from the agent's TOML profile) │
|
||||
│ 2. Code Context (generated): │
|
||||
│ ├─ File information (language, path) │
|
||||
│ ├─ Code before selection (configurable amount) │
|
||||
│ ├─ <selection_start> marker │
|
||||
│ ├─ Selected code (or current line) │
|
||||
│ ├─ <selection_end> marker │
|
||||
│ ├─ <cursor> marker (position within selection) │
|
||||
│ └─ Code after selection (configurable amount) │
|
||||
│ 4. Refactor Instruction: │
|
||||
│ ├─ Code after selection (configurable amount) │
|
||||
│ └─ Output formatting and indentation rules │
|
||||
│ 3. Refactor Instruction (the user message): │
|
||||
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
|
||||
│ ├─ Custom Instruction (from library) │
|
||||
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
|
||||
│ │ quick_refactor/instructions/*.json │
|
||||
│ └─ Additional Details (optional user input) │
|
||||
│ 5. Tool Definitions (if enabled) │
|
||||
│ 4. Tool Definitions (if the agent enables tools) │
|
||||
│ │
|
||||
│ Final Prompt: [System: SystemPrompt + Rules] │
|
||||
│ [User: Context + Markers + Instruction + Details] │
|
||||
│ Final Prompt: [System: Persona + CodeContext + Rules] │
|
||||
│ [User: Instruction + Details] │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -463,17 +452,15 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
||||
|
||||
### Key Points
|
||||
|
||||
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
||||
- **System Prompts** are configured independently for each feature in Settings
|
||||
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
|
||||
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
||||
- FIM models: Direct completion prompt
|
||||
- Non-FIM models: Prompt includes response formatting instructions
|
||||
- **Quick Refactor** has its own provider/model configuration, independent from Chat
|
||||
- **System Prompts** live in the agent's TOML profile (`system_prompt`); switch personas by switching agents
|
||||
- **Linked and open-synced files are always current**: their content is not stored in the conversation — every request re-reads the files and sends a fresh snapshot next to your latest message. Editing a linked file between messages never leaves a stale copy in the context, and changing it does not invalidate the provider's prompt cache for the whole conversation
|
||||
- **One-time attachments are different**: they are saved with the message they were sent with and stay in the history as sent
|
||||
- **FIM vs Non-FIM** for code completion is the agent's choice: a FIM agent renders prefix/suffix into native FIM fields, an instruct agent sends a chat-shaped request — pick the agent that matches your model
|
||||
- **Quick Refactor** has its own agent roster, independent from Chat
|
||||
- **Custom Instructions** provide reusable templates that can be augmented with specific details
|
||||
- **Tool Calling** is available for Chat and Quick Refactor when enabled
|
||||
- **Tool Calling** is available for Chat and Quick Refactor when the agent enables it; tool rounds per request are limited (configurable in `Settings → Tools`)
|
||||
|
||||
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||
See the [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||
|
||||
## QtCreator Version Compatibility
|
||||
|
||||
@@ -521,7 +508,6 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
|
||||
- [x] Diff sharing with models
|
||||
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
||||
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
|
||||
- [x] Project-specific rules (`.qodeassist/rules/`)
|
||||
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
||||
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
||||
- [ ] Full project source sharing
|
||||
@@ -532,7 +518,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
||||
|
||||
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
|
||||
|
||||
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features.
|
||||
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||
|
||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||
|
||||
@@ -580,6 +566,10 @@ cmake --build .
|
||||
|
||||
## For Contributors
|
||||
|
||||
### Adding an agent preset
|
||||
|
||||
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||
|
||||
### Code Style
|
||||
|
||||
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
||||
@@ -588,7 +578,46 @@ cmake --build .
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
|
||||
For detailed development guidelines and architecture patterns, see [docs/architecture.md](docs/architecture.md) and [docs/target-architecture.md](docs/target-architecture.md).
|
||||
|
||||
## License
|
||||
|
||||
QodeAssist is licensed under the **GNU General Public License v3.0**
|
||||
(see [`LICENSE`](LICENSE)), with **additional attribution terms under
|
||||
GPLv3 Section 7(b)**.
|
||||
|
||||
You are free to use, modify, and redistribute QodeAssist under GPL-3.0,
|
||||
but you **must preserve** the original author attribution, copyright
|
||||
notices, and project identification — including in source file headers,
|
||||
the plugin metadata (`QodeAssist.json.in`), and the About dialog or
|
||||
equivalent user-facing identification. Modified versions must be clearly
|
||||
marked as different from the original.
|
||||
|
||||
### Commercial licensing
|
||||
|
||||
QodeAssist is also available under a separate commercial license for use
|
||||
in proprietary or closed-source products without GPL-3.0 obligations.
|
||||
For commercial licensing inquiries, contact **palm1r-github-dev@pm.me**.
|
||||
|
||||
### Qt Creator components and attributions
|
||||
|
||||
QodeAssist is a plugin for Qt Creator and incorporates certain components
|
||||
(plugin templates, API headers, and related boilerplate) originating from
|
||||
Qt Creator, which are copyright (C) The Qt Company Ltd.
|
||||
|
||||
These components are provided by The Qt Company under the GNU General
|
||||
Public License version 3, annotated with **The Qt Company GPL Exception
|
||||
1.0**. This exception permits the development and distribution of Qt
|
||||
Creator plugins under licenses of the plugin author's own choosing,
|
||||
notwithstanding the GPL's general linking requirements. It is this
|
||||
exception that allows QodeAssist to be offered under both GPL-3.0 and a
|
||||
separate commercial license.
|
||||
|
||||
The original copyright and license notices of The Qt Company are
|
||||
preserved in the relevant source files and must not be removed.
|
||||
|
||||
For Qt Creator's licensing terms, see
|
||||
[LICENSE.GPL3-EXCEPT](https://github.com/qt-creator/qt-creator/blob/master/LICENSES/LICENSE.GPL3-EXCEPT).
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "RefactorSuggestion.hpp"
|
||||
#include "LLMSuggestion.hpp"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "RefactorSuggestionHoverHandler.hpp"
|
||||
#include "RefactorSuggestion.hpp"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -31,7 +32,6 @@ public:
|
||||
|
||||
void setSuggestionRange(const Utils::Text::Range &range);
|
||||
void clearSuggestionRange();
|
||||
bool hasSuggestion() const { return m_hasSuggestion; }
|
||||
|
||||
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
|
||||
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user