mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-02 19:19:13 -04:00
Compare commits
18 Commits
main
...
refactor-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35bbaa1af0 | ||
|
|
c070d65366 | ||
|
|
714b1367b7 | ||
|
|
f688b53703 | ||
|
|
2a3fd4f5be | ||
|
|
080947c0dc | ||
|
|
9cc57c602b | ||
|
|
4e3ecdd1f6 | ||
|
|
86135d0c13 | ||
|
|
d66c714a28 | ||
|
|
86c537477d | ||
|
|
70c6d30a72 | ||
|
|
747dfb540e | ||
|
|
e200278f9a | ||
|
|
755263c4de | ||
|
|
a6921f523a | ||
|
|
dc3100f054 | ||
|
|
ccc2ec2e80 |
@@ -2,10 +2,6 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
|
|
||||||
project(QodeAssist)
|
project(QodeAssist)
|
||||||
|
|
||||||
option(QODEASSIST_EXPERIMENTAL
|
|
||||||
"Enable experimental features" OFF)
|
|
||||||
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
|
|
||||||
|
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
@@ -42,7 +38,6 @@ add_definitions(
|
|||||||
|
|
||||||
add_subdirectory(sources)
|
add_subdirectory(sources)
|
||||||
add_subdirectory(logger)
|
add_subdirectory(logger)
|
||||||
add_subdirectory(pluginllmcore)
|
|
||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
add_subdirectory(UIControls)
|
add_subdirectory(UIControls)
|
||||||
add_subdirectory(ChatView)
|
add_subdirectory(ChatView)
|
||||||
@@ -69,7 +64,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::CPlusPlus
|
QtCreator::CPlusPlus
|
||||||
LLMQore
|
LLMQore
|
||||||
PluginLLMCore
|
|
||||||
ProvidersConfig
|
ProvidersConfig
|
||||||
Agents
|
Agents
|
||||||
Skills
|
Skills
|
||||||
@@ -83,42 +77,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssisttr.h
|
QodeAssisttr.h
|
||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
RefactorContextHelper.hpp
|
RefactorContextHelper.hpp
|
||||||
templates/Templates.hpp
|
|
||||||
templates/CodeLlamaFim.hpp
|
|
||||||
templates/Ollama.hpp
|
|
||||||
templates/Claude.hpp
|
|
||||||
templates/OpenAI.hpp
|
|
||||||
templates/MistralAI.hpp
|
|
||||||
templates/StarCoder2Fim.hpp
|
|
||||||
templates/Qwen25CoderFIM.hpp
|
|
||||||
templates/OpenAICompatible.hpp
|
|
||||||
templates/Llama3.hpp
|
|
||||||
templates/ChatML.hpp
|
|
||||||
templates/Alpaca.hpp
|
|
||||||
templates/Llama2.hpp
|
|
||||||
templates/CodeLlamaQMLFim.hpp
|
|
||||||
templates/GoogleAI.hpp
|
|
||||||
templates/LlamaCppFim.hpp
|
|
||||||
templates/Qwen3CoderFIM.hpp
|
|
||||||
templates/OpenAIResponses.hpp
|
|
||||||
providers/Providers.hpp
|
|
||||||
providers/ProviderUrlUtils.hpp
|
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
|
||||||
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
|
||||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
|
||||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
|
||||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
|
||||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
|
||||||
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
|
|
||||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
|
||||||
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
|
||||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
|
||||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
|
||||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
|
||||||
providers/QwenProvider.hpp providers/QwenProvider.cpp
|
|
||||||
providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp
|
|
||||||
providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp
|
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
@@ -130,17 +88,12 @@ add_qtc_plugin(QodeAssist
|
|||||||
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
||||||
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
||||||
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
||||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
|
||||||
CodeHandler.hpp CodeHandler.cpp
|
CodeHandler.hpp CodeHandler.cpp
|
||||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||||
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
|
||||||
widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp
|
widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp
|
||||||
widgets/CompletionHintWidget.hpp widgets/CompletionHintWidget.cpp
|
|
||||||
widgets/CompletionHintHandler.hpp widgets/CompletionHintHandler.cpp
|
|
||||||
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
|
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
|
||||||
widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp
|
widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp
|
||||||
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
|
|
||||||
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
|
|
||||||
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
||||||
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
||||||
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
||||||
@@ -170,10 +123,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if(QODEASSIST_EXPERIMENTAL)
|
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
|
||||||
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
|
|
||||||
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||||
find_program(QtCreatorExecutable
|
find_program(QtCreatorExecutable
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "AgentRoleController.hpp"
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "AgentRole.hpp"
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
AgentRoleController::AgentRoleController(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().systemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&AgentRoleController::baseSystemPromptChanged);
|
|
||||||
|
|
||||||
loadAvailableRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList AgentRoleController::availableRoles() const
|
|
||||||
{
|
|
||||||
return m_availableRoles;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRole() const
|
|
||||||
{
|
|
||||||
return m_currentRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::baseSystemPrompt() const
|
|
||||||
{
|
|
||||||
return Settings::chatAssistantSettings().systemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRoleDescription() const
|
|
||||||
{
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
if (lastRoleId.isEmpty())
|
|
||||||
return Settings::AgentRolesManager::getNoRole().description;
|
|
||||||
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (role.id.isEmpty())
|
|
||||||
return Settings::AgentRolesManager::getNoRole().description;
|
|
||||||
|
|
||||||
return role.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRoleSystemPrompt() const
|
|
||||||
{
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
if (lastRoleId.isEmpty())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (role.id.isEmpty())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
return role.systemPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::loadAvailableRoles()
|
|
||||||
{
|
|
||||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
|
||||||
|
|
||||||
m_availableRoles.clear();
|
|
||||||
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
|
||||||
|
|
||||||
for (const auto &role : roles)
|
|
||||||
m_availableRoles.append(role.name);
|
|
||||||
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
|
|
||||||
|
|
||||||
if (!lastRoleId.isEmpty()) {
|
|
||||||
for (const auto &role : roles) {
|
|
||||||
if (role.id == lastRoleId) {
|
|
||||||
m_currentRole = role.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit availableRolesChanged();
|
|
||||||
emit currentRoleChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::applyRole(const QString &roleName)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
|
||||||
|
|
||||||
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
|
||||||
settings.lastUsedRoleId.setValue("");
|
|
||||||
settings.writeSettings();
|
|
||||||
m_currentRole = roleName;
|
|
||||||
emit currentRoleChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
|
||||||
|
|
||||||
for (const auto &role : roles) {
|
|
||||||
if (role.name == roleName) {
|
|
||||||
settings.lastUsedRoleId.setValue(role.id);
|
|
||||||
settings.writeSettings();
|
|
||||||
m_currentRole = role.name;
|
|
||||||
emit currentRoleChanged();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::openSettings()
|
|
||||||
{
|
|
||||||
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class AgentRoleController : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit AgentRoleController(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QStringList availableRoles() const;
|
|
||||||
QString currentRole() const;
|
|
||||||
QString baseSystemPrompt() const;
|
|
||||||
QString currentRoleDescription() const;
|
|
||||||
QString currentRoleSystemPrompt() const;
|
|
||||||
|
|
||||||
void loadAvailableRoles();
|
|
||||||
void applyRole(const QString &roleName);
|
|
||||||
void openSettings();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void availableRolesChanged();
|
|
||||||
void currentRoleChanged();
|
|
||||||
void baseSystemPromptChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QStringList m_availableRoles;
|
|
||||||
QString m_currentRole;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -22,7 +22,6 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
qml/controls/BottomBar.qml
|
qml/controls/BottomBar.qml
|
||||||
qml/controls/FileMentionPopup.qml
|
qml/controls/FileMentionPopup.qml
|
||||||
qml/controls/FileEditsActionBar.qml
|
qml/controls/FileEditsActionBar.qml
|
||||||
qml/controls/ContextViewer.qml
|
|
||||||
qml/controls/SkillCommandPopup.qml
|
qml/controls/SkillCommandPopup.qml
|
||||||
qml/controls/Toast.qml
|
qml/controls/Toast.qml
|
||||||
qml/controls/TopBar.qml
|
qml/controls/TopBar.qml
|
||||||
@@ -47,8 +46,6 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/chat-pause-icon.svg
|
icons/chat-pause-icon.svg
|
||||||
icons/warning-icon.svg
|
icons/warning-icon.svg
|
||||||
icons/new-chat-icon.svg
|
icons/new-chat-icon.svg
|
||||||
icons/rules-icon.svg
|
|
||||||
icons/context-icon.svg
|
|
||||||
icons/open-in-editor.svg
|
icons/open-in-editor.svg
|
||||||
icons/open-in-window.svg
|
icons/open-in-window.svg
|
||||||
icons/apply-changes-button.svg
|
icons/apply-changes-button.svg
|
||||||
@@ -75,8 +72,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
ChatFileManager.hpp ChatFileManager.cpp
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
ChatCompressor.hpp ChatCompressor.cpp
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
AgentRoleController.hpp AgentRoleController.cpp
|
ChatAgentController.hpp ChatAgentController.cpp
|
||||||
ChatConfigurationController.hpp ChatConfigurationController.cpp
|
|
||||||
FileEditController.hpp FileEditController.cpp
|
FileEditController.hpp FileEditController.cpp
|
||||||
InputTokenCounter.hpp InputTokenCounter.cpp
|
InputTokenCounter.hpp InputTokenCounter.cpp
|
||||||
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
||||||
@@ -92,13 +88,14 @@ target_link_libraries(QodeAssistChatView
|
|||||||
Qt::Network
|
Qt::Network
|
||||||
QtCreator::Core
|
QtCreator::Core
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
PluginLLMCore
|
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
Context
|
Context
|
||||||
QodeAssistUIControlsplugin
|
QodeAssistUIControlsplugin
|
||||||
QodeAssistLogger
|
QodeAssistLogger
|
||||||
LLMQore
|
LLMQore
|
||||||
Skills
|
Skills
|
||||||
|
Agents
|
||||||
|
Session
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
|
|||||||
114
ChatView/ChatAgentController.cpp
Normal file
114
ChatView/ChatAgentController.cpp
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include "ChatAgentController.hpp"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <sources/settings/PipelinesConfig.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatAgentController::ChatAgentController(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
if (auto *settings = Core::ICore::settings())
|
||||||
|
m_currentAgent = settings->value(kChatAgentKey).toString();
|
||||||
|
|
||||||
|
connect(
|
||||||
|
Settings::PipelinesNotifier::instance(),
|
||||||
|
&Settings::PipelinesNotifier::pipelinesChanged,
|
||||||
|
this,
|
||||||
|
&ChatAgentController::reload);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatAgentController::setAgentFactory(AgentFactory *factory)
|
||||||
|
{
|
||||||
|
m_agentFactory = factory;
|
||||||
|
if (factory)
|
||||||
|
connect(
|
||||||
|
factory,
|
||||||
|
&AgentFactory::agentsChanged,
|
||||||
|
this,
|
||||||
|
&ChatAgentController::reload,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatAgentController::availableAgents() const
|
||||||
|
{
|
||||||
|
return m_availableAgents;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatAgentController::currentAgent() const
|
||||||
|
{
|
||||||
|
return m_currentAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatAgentController::setCurrentAgent(const QString &name)
|
||||||
|
{
|
||||||
|
if (name == m_currentAgent || !m_availableAgents.contains(name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
applyCurrentAgent(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatAgentController::reload()
|
||||||
|
{
|
||||||
|
if (!m_agentFactory)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QStringList all = m_agentFactory->configNames();
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatAssistant;
|
||||||
|
|
||||||
|
QStringList filtered;
|
||||||
|
for (const QString &name : roster) {
|
||||||
|
if (all.contains(name))
|
||||||
|
filtered.append(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtered != m_availableAgents) {
|
||||||
|
m_availableAgents = filtered;
|
||||||
|
emit availableAgentsChanged();
|
||||||
|
}
|
||||||
|
ensureValidCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatAgentController::ensureValidCurrent()
|
||||||
|
{
|
||||||
|
if (m_availableAgents.contains(m_currentAgent))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString next = m_availableAgents.isEmpty() ? QString() : m_availableAgents.first();
|
||||||
|
if (next == m_currentAgent)
|
||||||
|
return;
|
||||||
|
|
||||||
|
applyCurrentAgent(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatAgentController::applyCurrentAgent(const QString &name)
|
||||||
|
{
|
||||||
|
m_currentAgent = name;
|
||||||
|
if (auto *settings = Core::ICore::settings())
|
||||||
|
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||||
|
emit currentAgentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatAgentController::currentSupportsTools() const
|
||||||
|
{
|
||||||
|
if (!m_agentFactory || m_currentAgent.isEmpty())
|
||||||
|
return false;
|
||||||
|
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
|
||||||
|
return config && config->enableTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
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
|
||||||
@@ -4,19 +4,32 @@
|
|||||||
|
|
||||||
#include "ChatCompressor.hpp"
|
#include "ChatCompressor.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include "ChatModel.hpp"
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "PromptTemplateManager.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <ContextRenderer.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QStringList>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@@ -25,7 +38,17 @@ ChatCompressor::ChatCompressor(QObject *parent)
|
|||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
void ChatCompressor::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::setActiveAgent(const QString &agentName)
|
||||||
|
{
|
||||||
|
m_activeAgent = agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory)
|
||||||
{
|
{
|
||||||
if (m_isCompressing) {
|
if (m_isCompressing) {
|
||||||
emit compressionFailed(tr("Compression already in progress"));
|
emit compressionFailed(tr("Compression already in progress"));
|
||||||
@@ -37,49 +60,85 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatModel || chatModel->rowCount() == 0) {
|
if (!sourceHistory || sourceHistory->isEmpty()) {
|
||||||
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
if (!m_sessionManager) {
|
||||||
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
emit compressionFailed(tr("Chat session manager is not available"));
|
||||||
|
|
||||||
if (!m_provider) {
|
|
||||||
emit compressionFailed(tr("No provider available"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().caTemplate();
|
QString sessionError;
|
||||||
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError);
|
||||||
templateName);
|
if (!session) {
|
||||||
|
emit compressionFailed(sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
|
||||||
if (!promptTemplate) {
|
|
||||||
emit compressionFailed(tr("No template available"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto *client = session->client();
|
||||||
|
if (!client) {
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emit compressionFailed(tr("Chat agent has no live client"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
|
Templates::ContextRenderer::Bindings bindings;
|
||||||
|
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
||||||
|
bindings.configDir = AgentFactory::userConfigDir();
|
||||||
|
session->setContextBindings(bindings);
|
||||||
|
|
||||||
m_isCompressing = true;
|
m_isCompressing = true;
|
||||||
m_chatModel = chatModel;
|
|
||||||
m_originalChatPath = chatFilePath;
|
m_originalChatPath = chatFilePath;
|
||||||
m_accumulatedSummary.clear();
|
m_session = session;
|
||||||
|
|
||||||
emit compressionStarted();
|
emit compressionStarted();
|
||||||
|
|
||||||
connectProviderSignals();
|
QStringList transcriptParts;
|
||||||
|
for (const auto &msg : sourceHistory->messages()) {
|
||||||
|
if (msg.role() != Message::Role::User && msg.role() != Message::Role::Assistant)
|
||||||
|
continue;
|
||||||
|
const QString text = msg.text();
|
||||||
|
if (text.trimmed().isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
QJsonObject payload{
|
const QString role = msg.role() == Message::Role::User ? QStringLiteral("User")
|
||||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
: QStringLiteral("Assistant");
|
||||||
|
transcriptParts.append(QStringLiteral("%1: %2").arg(role, text));
|
||||||
|
}
|
||||||
|
|
||||||
buildRequestPayload(payload, promptTemplate);
|
if (transcriptParts.isEmpty()) {
|
||||||
|
handleCompressionError(tr("Chat is empty, nothing to compress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
const QString transcript = transcriptParts.join(QStringLiteral("\n\n"));
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
|
||||||
: promptTemplate->endpoint();
|
connect(session, &Session::finished, this, [this](const LLMQore::RequestID &id, const QString &) {
|
||||||
m_provider->client()->setTransferTimeout(
|
onCompressionFinished(id);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
session,
|
||||||
|
&Session::failed,
|
||||||
|
this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onCompressionFailed(id, error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
client->setTransferTimeout(
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
m_currentRequestId = m_provider->sendRequest(
|
|
||||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(transcript));
|
||||||
|
|
||||||
|
m_currentRequestId = session->send(std::move(blocks));
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
handleCompressionError(
|
||||||
|
tr("Failed to start compression request: %1").arg(session->lastError().message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,44 +153,43 @@ void ChatCompressor::cancelCompression()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
LOG_MESSAGE("Cancelling compression request");
|
LOG_MESSAGE("Cancelling compression request");
|
||||||
|
|
||||||
if (m_provider && !m_currentRequestId.isEmpty())
|
|
||||||
m_provider->cancelRequest(m_currentRequestId);
|
|
||||||
|
|
||||||
cleanupState();
|
cleanupState();
|
||||||
emit compressionFailed(tr("Compression cancelled"));
|
emit compressionFailed(tr("Compression cancelled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
void ChatCompressor::onCompressionFinished(const QString &requestId)
|
||||||
{
|
{
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_accumulatedSummary += partialText;
|
QString summary;
|
||||||
}
|
if (m_session) {
|
||||||
|
if (auto *history = m_session->history(); history && !history->isEmpty())
|
||||||
|
summary = history->messages().back().text();
|
||||||
|
}
|
||||||
|
|
||||||
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length()));
|
||||||
{
|
|
||||||
Q_UNUSED(fullText)
|
|
||||||
|
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
if (summary.trimmed().isEmpty()) {
|
||||||
|
handleCompressionError(tr("Compression produced an empty summary"));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(
|
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
const QString sourcePath = m_originalChatPath;
|
||||||
|
|
||||||
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
cleanupState();
|
||||||
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
|
||||||
handleCompressionError(tr("Failed to save compressed chat"));
|
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
|
||||||
|
emit compressionFailed(tr("Failed to save compressed chat"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||||
cleanupState();
|
|
||||||
emit compressionCompleted(compressedPath);
|
emit compressionCompleted(compressedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error)
|
||||||
{
|
{
|
||||||
if (!m_isCompressing || requestId != m_currentRequestId)
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
return;
|
return;
|
||||||
@@ -154,81 +212,19 @@ QString ChatCompressor::createCompressedChatPath(const QString &originalPath) co
|
|||||||
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
|
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatCompressor::buildCompressionPrompt() const
|
|
||||||
{
|
|
||||||
return QStringLiteral(
|
|
||||||
"Please create a comprehensive summary of our entire conversation above. "
|
|
||||||
"The summary should:\n"
|
|
||||||
"1. Preserve all important context, decisions, and key information\n"
|
|
||||||
"2. Maintain technical details, code snippets, file references, and specific examples\n"
|
|
||||||
"3. Keep the chronological flow of the discussion\n"
|
|
||||||
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
|
|
||||||
"5. Be written in clear, structured format\n"
|
|
||||||
"6. Use markdown formatting for better readability\n\n"
|
|
||||||
"Create the summary now:");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatCompressor::buildRequestPayload(
|
|
||||||
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
|
||||||
{
|
|
||||||
PluginLLMCore::ContextData context;
|
|
||||||
|
|
||||||
context.systemPrompt = QStringLiteral(
|
|
||||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
|
||||||
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
|
||||||
if (msg.role == ChatModel::ChatRole::Tool
|
|
||||||
|| msg.role == ChatModel::ChatRole::FileEdit
|
|
||||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
PluginLLMCore::Message apiMessage;
|
|
||||||
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
|
||||||
apiMessage.content = msg.content;
|
|
||||||
messages.append(apiMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::Message compressionRequest;
|
|
||||||
compressionRequest.role = "user";
|
|
||||||
compressionRequest.content = buildCompressionPrompt();
|
|
||||||
messages.append(compressionRequest);
|
|
||||||
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
m_provider->prepareRequest(
|
|
||||||
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatCompressor::createCompressedChatFile(
|
bool ChatCompressor::createCompressedChatFile(
|
||||||
const QString &sourcePath, const QString &destPath, const QString &summary)
|
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||||
{
|
{
|
||||||
QFile sourceFile(sourcePath);
|
QJsonObject root;
|
||||||
if (!sourceFile.open(QIODevice::ReadOnly)) {
|
root["version"] = ChatSerializer::VERSION;
|
||||||
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonParseError parseError;
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
|
|
||||||
sourceFile.close();
|
|
||||||
|
|
||||||
if (doc.isNull() || !doc.isObject()) {
|
|
||||||
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
|
|
||||||
.arg(sourcePath, parseError.errorString()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject root = doc.object();
|
|
||||||
|
|
||||||
QJsonObject summaryMessage;
|
QJsonObject summaryMessage;
|
||||||
summaryMessage["role"] = "assistant";
|
summaryMessage["role"] = "assistant";
|
||||||
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
|
||||||
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
summaryMessage["isRedacted"] = false;
|
QJsonObject textBlock;
|
||||||
summaryMessage["attachments"] = QJsonArray();
|
textBlock["type"] = "text";
|
||||||
summaryMessage["images"] = QJsonArray();
|
textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||||
|
summaryMessage["blocks"] = QJsonArray{textBlock};
|
||||||
|
|
||||||
root["messages"] = QJsonArray{summaryMessage};
|
root["messages"] = QJsonArray{summaryMessage};
|
||||||
root["compressedFrom"] = sourcePath;
|
root["compressedFrom"] = sourcePath;
|
||||||
@@ -247,49 +243,17 @@ bool ChatCompressor::createCompressedChatFile(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatCompressor::connectProviderSignals()
|
|
||||||
{
|
|
||||||
auto *c = m_provider->client();
|
|
||||||
|
|
||||||
m_connections.append(connect(
|
|
||||||
c,
|
|
||||||
&::LLMQore::BaseClient::chunkReceived,
|
|
||||||
this,
|
|
||||||
&ChatCompressor::onPartialResponseReceived,
|
|
||||||
Qt::UniqueConnection));
|
|
||||||
|
|
||||||
m_connections.append(connect(
|
|
||||||
c,
|
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
|
||||||
this,
|
|
||||||
&ChatCompressor::onFullResponseReceived,
|
|
||||||
Qt::UniqueConnection));
|
|
||||||
|
|
||||||
m_connections.append(connect(
|
|
||||||
c,
|
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
|
||||||
this,
|
|
||||||
&ChatCompressor::onRequestFailed,
|
|
||||||
Qt::UniqueConnection));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatCompressor::disconnectAllSignals()
|
|
||||||
{
|
|
||||||
for (const auto &connection : std::as_const(m_connections))
|
|
||||||
disconnect(connection);
|
|
||||||
m_connections.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatCompressor::cleanupState()
|
void ChatCompressor::cleanupState()
|
||||||
{
|
{
|
||||||
disconnectAllSignals();
|
Session *session = m_session;
|
||||||
|
|
||||||
m_isCompressing = false;
|
m_isCompressing = false;
|
||||||
m_currentRequestId.clear();
|
m_currentRequestId.clear();
|
||||||
m_originalChatPath.clear();
|
m_originalChatPath.clear();
|
||||||
m_accumulatedSummary.clear();
|
m_session = nullptr;
|
||||||
m_chatModel = nullptr;
|
|
||||||
m_provider = nullptr;
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -4,20 +4,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
namespace QodeAssist::PluginLLMCore {
|
namespace QodeAssist {
|
||||||
class Provider;
|
class SessionManager;
|
||||||
class PromptTemplate;
|
class Session;
|
||||||
} // namespace QodeAssist::PluginLLMCore
|
class ConversationHistory;
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class ChatCompressor : public QObject
|
class ChatCompressor : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -25,7 +24,10 @@ class ChatCompressor : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ChatCompressor(QObject *parent = nullptr);
|
explicit ChatCompressor(QObject *parent = nullptr);
|
||||||
|
|
||||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setActiveAgent(const QString &agentName);
|
||||||
|
|
||||||
|
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
|
||||||
|
|
||||||
bool isCompressing() const;
|
bool isCompressing() const;
|
||||||
void cancelCompression();
|
void cancelCompression();
|
||||||
@@ -35,30 +37,22 @@ signals:
|
|||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
void compressionFailed(const QString &error);
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
|
||||||
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
|
||||||
void onRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void onCompressionFinished(const QString &requestId);
|
||||||
|
void onCompressionFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
QString createCompressedChatPath(const QString &originalPath) const;
|
QString createCompressedChatPath(const QString &originalPath) const;
|
||||||
QString buildCompressionPrompt() const;
|
|
||||||
bool createCompressedChatFile(
|
bool createCompressedChatFile(
|
||||||
const QString &sourcePath, const QString &destPath, const QString &summary);
|
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||||
void connectProviderSignals();
|
|
||||||
void disconnectAllSignals();
|
|
||||||
void cleanupState();
|
void cleanupState();
|
||||||
void handleCompressionError(const QString &error);
|
void handleCompressionError(const QString &error);
|
||||||
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
|
||||||
|
|
||||||
bool m_isCompressing = false;
|
bool m_isCompressing = false;
|
||||||
QString m_currentRequestId;
|
QString m_currentRequestId;
|
||||||
QString m_originalChatPath;
|
QString m_originalChatPath;
|
||||||
QString m_accumulatedSummary;
|
QPointer<SessionManager> m_sessionManager;
|
||||||
PluginLLMCore::Provider *m_provider = nullptr;
|
QString m_activeAgent;
|
||||||
ChatModel *m_chatModel = nullptr;
|
QPointer<Session> m_session;
|
||||||
|
|
||||||
QList<QMetaObject::Connection> m_connections;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatConfigurationController.hpp"
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
ChatConfigurationController::ChatConfigurationController(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
connect(
|
|
||||||
&settings.caProvider,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatConfigurationController::updateCurrentConfiguration);
|
|
||||||
connect(
|
|
||||||
&settings.caModel,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatConfigurationController::updateCurrentConfiguration);
|
|
||||||
|
|
||||||
loadAvailableConfigurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChatConfigurationController::availableConfigurations() const
|
|
||||||
{
|
|
||||||
return m_availableConfigurations;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatConfigurationController::currentConfiguration() const
|
|
||||||
{
|
|
||||||
return m_currentConfiguration;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::updateCurrentConfiguration()
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
m_currentConfiguration
|
|
||||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::loadAvailableConfigurations()
|
|
||||||
{
|
|
||||||
auto &manager = Settings::ConfigurationManager::instance();
|
|
||||||
manager.loadConfigurations(Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
|
||||||
Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
m_availableConfigurations.clear();
|
|
||||||
m_availableConfigurations.append(QObject::tr("Current Settings"));
|
|
||||||
|
|
||||||
for (const Settings::AIConfiguration &config : configs) {
|
|
||||||
m_availableConfigurations.append(config.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentConfiguration();
|
|
||||||
|
|
||||||
emit availableConfigurationsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::applyConfiguration(const QString &configName)
|
|
||||||
{
|
|
||||||
if (configName == QObject::tr("Current Settings")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &manager = Settings::ConfigurationManager::instance();
|
|
||||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
|
||||||
Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
for (const Settings::AIConfiguration &config : configs) {
|
|
||||||
if (config.name == configName) {
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
|
|
||||||
settings.caProvider.setValue(config.provider);
|
|
||||||
settings.caModel.setValue(config.model);
|
|
||||||
settings.caTemplate.setValue(config.templateName);
|
|
||||||
settings.caUrl.setValue(config.url);
|
|
||||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
|
||||||
|
|
||||||
settings.writeSettings();
|
|
||||||
|
|
||||||
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class ChatConfigurationController : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ChatConfigurationController(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QStringList availableConfigurations() const;
|
|
||||||
QString currentConfiguration() const;
|
|
||||||
|
|
||||||
void loadAvailableConfigurations();
|
|
||||||
void applyConfiguration(const QString &configName);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void availableConfigurationsChanged();
|
|
||||||
void currentConfigurationChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateCurrentConfiguration();
|
|
||||||
|
|
||||||
QStringList m_availableConfigurations;
|
|
||||||
QString m_currentConfiguration;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -16,14 +16,22 @@
|
|||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
|
ChatHistoryStore::ChatHistoryStore(
|
||||||
|
ConversationHistory *history, ChatModel *chatModel, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
, m_history(history)
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
@@ -52,19 +60,25 @@ QString ChatHistoryStore::suggestedFileName() const
|
|||||||
{
|
{
|
||||||
QString shortMessage;
|
QString shortMessage;
|
||||||
|
|
||||||
if (m_chatModel->rowCount() > 0) {
|
if (m_history) {
|
||||||
QString firstMessage
|
for (const auto &message : m_history->messages()) {
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
if (message.role() != Message::Role::User)
|
||||||
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
continue;
|
||||||
|
|
||||||
if (shortMessage.isEmpty()) {
|
const QString text = message.text();
|
||||||
QVariantList images
|
if (!text.trimmed().isEmpty()) {
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
shortMessage = text.split('\n').first().simplified().left(30);
|
||||||
if (!images.isEmpty()) {
|
} else {
|
||||||
|
for (const auto &block : message.blocks()) {
|
||||||
|
if (dynamic_cast<StoredImageContent *>(block.get())) {
|
||||||
shortMessage = "image_chat";
|
shortMessage = "image_chat";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return generateChatFileName(shortMessage, historyDir());
|
return generateChatFileName(shortMessage, historyDir());
|
||||||
}
|
}
|
||||||
@@ -107,12 +121,17 @@ QString ChatHistoryStore::autosaveFilePath(
|
|||||||
|
|
||||||
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return ChatSerializer::saveToFile(m_chatModel, filePath);
|
return ChatSerializer::saveToFile(
|
||||||
|
m_history, filePath, m_chatModel ? m_chatModel->usageToJson() : QJsonObject{});
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return ChatSerializer::loadFromFile(m_chatModel, filePath);
|
QJsonObject usage;
|
||||||
|
const SerializationResult result = ChatSerializer::loadFromFile(m_history, filePath, &usage);
|
||||||
|
if (result.success && m_chatModel)
|
||||||
|
m_chatModel->restoreUsageFromJson(usage);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatHistoryStore::showSaveDialog()
|
void ChatHistoryStore::showSaveDialog()
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
class ChatModel;
|
||||||
@@ -18,7 +22,8 @@ class ChatHistoryStore : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit ChatHistoryStore(
|
||||||
|
ConversationHistory *history, ChatModel *chatModel, QObject *parent = nullptr);
|
||||||
|
|
||||||
QString historyDir() const;
|
QString historyDir() const;
|
||||||
QString suggestedFileName() const;
|
QString suggestedFileName() const;
|
||||||
@@ -42,6 +47,7 @@ signals:
|
|||||||
private:
|
private:
|
||||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||||
|
|
||||||
|
ConversationHistory *m_history;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,16 @@
|
|||||||
#include "MessagePart.hpp"
|
#include "MessagePart.hpp"
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
#include <QHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QVector>
|
||||||
#include <QtQmlIntegration>
|
#include <QtQmlIntegration>
|
||||||
|
|
||||||
#include "context/ContentFile.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -21,15 +26,15 @@ class ChatModel : public QAbstractListModel
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
|
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||||
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
|
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
|
||||||
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
|
Q_PROPERTY(
|
||||||
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
|
int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
enum ChatRole : int { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||||
Q_ENUM(ChatRole)
|
Q_ENUM(ChatRole)
|
||||||
|
|
||||||
enum Roles {
|
enum Roles : int {
|
||||||
RoleType = Qt::UserRole,
|
RoleType = Qt::UserRole,
|
||||||
Content,
|
Content,
|
||||||
Attachments,
|
Attachments,
|
||||||
@@ -43,81 +48,19 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
struct ImageAttachment
|
|
||||||
{
|
|
||||||
QString fileName; // Original filename
|
|
||||||
QString storedPath; // Path to stored image file (relative to chat folder)
|
|
||||||
QString mediaType; // MIME type
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Message
|
|
||||||
{
|
|
||||||
ChatRole role;
|
|
||||||
QString content;
|
|
||||||
QString id;
|
|
||||||
bool isRedacted = false;
|
|
||||||
QString signature = QString();
|
|
||||||
|
|
||||||
QList<Context::ContentFile> attachments;
|
|
||||||
QList<ImageAttachment> images;
|
|
||||||
|
|
||||||
QString toolName;
|
|
||||||
QJsonObject toolArguments;
|
|
||||||
QString toolResult;
|
|
||||||
|
|
||||||
int promptTokens = 0;
|
|
||||||
int completionTokens = 0;
|
|
||||||
int cachedPromptTokens = 0;
|
|
||||||
int reasoningTokens = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit ChatModel(QObject *parent = nullptr);
|
explicit ChatModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setHistory(ConversationHistory *history);
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
Q_INVOKABLE void addMessage(
|
|
||||||
const QString &content,
|
|
||||||
ChatRole role,
|
|
||||||
const QString &id,
|
|
||||||
const QList<Context::ContentFile> &attachments = {},
|
|
||||||
const QList<ImageAttachment> &images = {},
|
|
||||||
bool isRedacted = false,
|
|
||||||
const QString &signature = QString());
|
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||||
|
|
||||||
QVector<Message> getChatHistory() const;
|
|
||||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
|
||||||
|
|
||||||
QString currentModel() const;
|
|
||||||
QString lastMessageId() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void resetModelTo(int index);
|
Q_INVOKABLE void resetModelTo(int index);
|
||||||
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
||||||
|
|
||||||
void addToolExecutionStatus(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments);
|
|
||||||
void dropTrailingAssistantMessage(const QString &requestId);
|
|
||||||
void setToolMessageData(
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments,
|
|
||||||
const QString &toolResult);
|
|
||||||
void updateToolResult(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &result);
|
|
||||||
void addThinkingBlock(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
|
||||||
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
|
||||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
|
||||||
|
|
||||||
void setMessageUsage(
|
void setMessageUsage(
|
||||||
const QString &messageId,
|
const QString &messageId,
|
||||||
int promptTokens,
|
int promptTokens,
|
||||||
@@ -125,13 +68,12 @@ public:
|
|||||||
int cachedPromptTokens,
|
int cachedPromptTokens,
|
||||||
int reasoningTokens);
|
int reasoningTokens);
|
||||||
|
|
||||||
|
QJsonObject usageToJson() const;
|
||||||
|
void restoreUsageFromJson(const QJsonObject &usage);
|
||||||
|
|
||||||
int sessionPromptTokens() const;
|
int sessionPromptTokens() const;
|
||||||
int sessionCompletionTokens() const;
|
int sessionCompletionTokens() const;
|
||||||
int sessionCachedPromptTokens() const;
|
int sessionCachedPromptTokens() const;
|
||||||
int sessionTotalTokens() const;
|
|
||||||
|
|
||||||
void setLoadingFromHistory(bool loading);
|
|
||||||
bool isLoadingFromHistory() const;
|
|
||||||
|
|
||||||
void setChatFilePath(const QString &filePath);
|
void setChatFilePath(const QString &filePath);
|
||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
@@ -141,18 +83,64 @@ signals:
|
|||||||
void sessionUsageChanged();
|
void sessionUsageChanged();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onFileEditApplied(const QString &editId);
|
void onHistoryMessageAdded(int index);
|
||||||
void onFileEditRejected(const QString &editId);
|
void onHistoryMessageUpdated(int index);
|
||||||
void onFileEditArchived(const QString &editId);
|
void onHistoryCleared();
|
||||||
|
void onHistoryReset();
|
||||||
|
void onFileEditStatusChanged(const QString &editId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
struct AttachmentRef
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString storedPath;
|
||||||
|
};
|
||||||
|
struct ImageRef
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString storedPath;
|
||||||
|
QString mediaType;
|
||||||
|
};
|
||||||
|
struct Row
|
||||||
|
{
|
||||||
|
ChatRole kind = ChatRole::Assistant;
|
||||||
|
int messageIndex = -1;
|
||||||
|
QString messageId;
|
||||||
|
QString content;
|
||||||
|
bool isRedacted = false;
|
||||||
|
QString editId;
|
||||||
|
QString fileEditDisplay;
|
||||||
|
QVector<AttachmentRef> attachments;
|
||||||
|
QVector<ImageRef> images;
|
||||||
|
};
|
||||||
|
struct Usage
|
||||||
|
{
|
||||||
|
int prompt = 0;
|
||||||
|
int completion = 0;
|
||||||
|
int cached = 0;
|
||||||
|
int reasoning = 0;
|
||||||
|
};
|
||||||
|
|
||||||
QVector<Message> m_messages;
|
void rebuildAll();
|
||||||
bool m_loadingFromHistory = false;
|
void reprojectTail(int startMessageIndex);
|
||||||
|
int startMessageIndexFor(int messageIndex) const;
|
||||||
|
int firstRowForMessage(int messageIndex) const;
|
||||||
|
QHash<QString, QString> buildToolResultMap() const;
|
||||||
|
void mergeToolResultsFromMessage(int messageIndex);
|
||||||
|
void pruneUsageToHistory();
|
||||||
|
void appendRowsForMessage(
|
||||||
|
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
|
||||||
|
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
|
||||||
|
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
|
||||||
|
QVariantList buildImageList(const QVector<ImageRef> &images) const;
|
||||||
|
|
||||||
|
QPointer<ConversationHistory> m_history;
|
||||||
|
QVector<Row> m_rows;
|
||||||
|
QHash<QString, QString> m_toolResults;
|
||||||
|
QHash<QString, Usage> m_usageByMessageId;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
|
|
||||||
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)
|
||||||
|
|||||||
@@ -28,22 +28,25 @@
|
|||||||
|
|
||||||
#include "QodeAssistConstants.hpp"
|
#include "QodeAssistConstants.hpp"
|
||||||
|
|
||||||
#include "AgentRoleController.hpp"
|
#include <AgentFactory.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <sources/settings/PipelinesConfig.hpp>
|
||||||
|
|
||||||
|
#include "ChatAgentController.hpp"
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatConfigurationController.hpp"
|
|
||||||
#include "ChatCompressor.hpp"
|
#include "ChatCompressor.hpp"
|
||||||
#include "ChatHistoryStore.hpp"
|
#include "ChatHistoryStore.hpp"
|
||||||
#include "FileEditController.hpp"
|
#include "FileEditController.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "InputTokenCounter.hpp"
|
#include "InputTokenCounter.hpp"
|
||||||
#include "SettingsConstants.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "SessionFileRegistry.hpp"
|
|
||||||
#include "context/ContextManager.hpp"
|
|
||||||
#include "pluginllmcore/RulesLoader.hpp"
|
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
|
#include "SessionFileRegistry.hpp"
|
||||||
|
#include "SettingsConstants.hpp"
|
||||||
#include "SkillsSettings.hpp"
|
#include "SkillsSettings.hpp"
|
||||||
|
#include "context/ContextManager.hpp"
|
||||||
#include "sources/skills/SkillsManager.hpp"
|
#include "sources/skills/SkillsManager.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@@ -73,19 +76,20 @@ QKeySequence sendMessageKeySequence()
|
|||||||
|
|
||||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||||
: QQuickItem(parent)
|
: QQuickItem(parent)
|
||||||
|
, m_history(new QodeAssist::ConversationHistory(this))
|
||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
, m_clientInterface(new ClientInterface(m_chatModel, this))
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
|
||||||
, m_fileManager(new ChatFileManager(this))
|
, m_fileManager(new ChatFileManager(this))
|
||||||
, m_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
, m_chatCompressor(new ChatCompressor(this))
|
, m_chatCompressor(new ChatCompressor(this))
|
||||||
, m_agentRoleController(new AgentRoleController(this))
|
, m_agentController(new ChatAgentController(this))
|
||||||
, m_configurationController(new ChatConfigurationController(this))
|
, m_fileEditController(new FileEditController(this))
|
||||||
, m_fileEditController(new FileEditController(m_chatModel, this))
|
, m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
|
||||||
, m_tokenCounter(
|
, m_historyStore(new ChatHistoryStore(m_history, m_chatModel, this))
|
||||||
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
|
|
||||||
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
|
|
||||||
{
|
{
|
||||||
|
m_chatModel->setHistory(m_history);
|
||||||
|
m_clientInterface->setHistory(m_history);
|
||||||
|
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(
|
||||||
&Settings::chatAssistantSettings().linkOpenFiles,
|
&Settings::chatAssistantSettings().linkOpenFiles,
|
||||||
@@ -109,22 +113,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
m_configurationController,
|
|
||||||
&ChatConfigurationController::availableConfigurationsChanged,
|
|
||||||
this,
|
|
||||||
&ChatRootView::availableConfigurationsChanged);
|
|
||||||
connect(
|
|
||||||
m_configurationController,
|
|
||||||
&ChatConfigurationController::currentConfigurationChanged,
|
|
||||||
this,
|
|
||||||
&ChatRootView::currentConfigurationChanged);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
&ClientInterface::messageReceivedCompletely,
|
&ClientInterface::messageReceivedCompletely,
|
||||||
@@ -143,6 +131,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
|
|
||||||
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
||||||
setRecentFilePath(QString{});
|
setRecentFilePath(QString{});
|
||||||
|
m_tokenCounter->resetServerUsage();
|
||||||
m_fileEditController->clearCurrentRequestId();
|
m_fileEditController->clearCurrentRequestId();
|
||||||
});
|
});
|
||||||
auto maybeEmitTitle = [this] {
|
auto maybeEmitTitle = [this] {
|
||||||
@@ -156,7 +145,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
connect(m_chatModel, &QAbstractItemModel::modelReset, this, maybeEmitTitle);
|
connect(m_chatModel, &QAbstractItemModel::modelReset, this, maybeEmitTitle);
|
||||||
connect(m_chatModel, &QAbstractItemModel::rowsInserted, this, maybeEmitTitle);
|
connect(m_chatModel, &QAbstractItemModel::rowsInserted, this, maybeEmitTitle);
|
||||||
connect(m_chatModel, &QAbstractItemModel::rowsRemoved, this, maybeEmitTitle);
|
connect(m_chatModel, &QAbstractItemModel::rowsRemoved, this, maybeEmitTitle);
|
||||||
connect(m_chatModel, &QAbstractItemModel::dataChanged, this, maybeEmitTitle);
|
|
||||||
connect(this, &ChatRootView::attachmentFilesChanged, this, [this]() {
|
connect(this, &ChatRootView::attachmentFilesChanged, this, [this]() {
|
||||||
m_tokenCounter->setAttachments(m_attachmentFiles);
|
m_tokenCounter->setAttachments(m_attachmentFiles);
|
||||||
});
|
});
|
||||||
@@ -171,20 +159,28 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::inputTokensCountChanged);
|
&ChatRootView::inputTokensCountChanged);
|
||||||
connect(
|
connect(
|
||||||
m_agentRoleController,
|
m_agentController,
|
||||||
&AgentRoleController::availableRolesChanged,
|
&ChatAgentController::availableAgentsChanged,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::availableAgentRolesChanged);
|
&ChatRootView::availableChatAgentsChanged);
|
||||||
connect(
|
connect(
|
||||||
m_agentRoleController,
|
m_agentController,
|
||||||
&AgentRoleController::currentRoleChanged,
|
&ChatAgentController::currentAgentChanged,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::currentAgentRoleChanged);
|
&ChatRootView::currentChatAgentChanged);
|
||||||
connect(
|
connect(
|
||||||
m_agentRoleController,
|
m_agentController,
|
||||||
&AgentRoleController::baseSystemPromptChanged,
|
&ChatAgentController::currentAgentChanged,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::baseSystemPromptChanged);
|
&ChatRootView::useToolsChanged);
|
||||||
|
connect(
|
||||||
|
Settings::PipelinesNotifier::instance(),
|
||||||
|
&Settings::PipelinesNotifier::pipelinesChanged,
|
||||||
|
this,
|
||||||
|
[this]() {
|
||||||
|
emit availableChatAgentsChanged();
|
||||||
|
emit useToolsChanged();
|
||||||
|
});
|
||||||
|
|
||||||
auto editors = Core::EditorManager::instance();
|
auto editors = Core::EditorManager::instance();
|
||||||
|
|
||||||
@@ -234,17 +230,21 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
});
|
});
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
|
||||||
&ClientInterface::requestStarted,
|
m_fileEditController->setCurrentRequestId(requestId);
|
||||||
this,
|
setRequestProgressStatus(true);
|
||||||
[this](const QString &requestId) { m_fileEditController->setCurrentRequestId(requestId); });
|
});
|
||||||
|
|
||||||
|
connect(m_clientInterface, &ClientInterface::requestCancelled, this, [this]() {
|
||||||
|
setRequestProgressStatus(false);
|
||||||
|
});
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
&ClientInterface::messageUsageReceived,
|
&ClientInterface::messageUsageReceived,
|
||||||
this,
|
this,
|
||||||
[this](int promptTokens, int /*completionTokens*/, int /*cached*/, int /*reasoning*/) {
|
[this](int promptTokens, int /*completionTokens*/, int cachedTokens, int /*reasoning*/) {
|
||||||
m_tokenCounter->recordServerUsage(promptTokens);
|
m_tokenCounter->recordServerUsage(promptTokens, cachedTokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
@@ -266,14 +266,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
connect(
|
connect(
|
||||||
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
||||||
|
|
||||||
refreshRules();
|
|
||||||
|
|
||||||
connect(
|
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
|
||||||
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
|
||||||
this,
|
|
||||||
&ChatRootView::refreshRules);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
&ProjectExplorer::ProjectManager::projectAdded,
|
&ProjectExplorer::ProjectManager::projectAdded,
|
||||||
@@ -286,24 +278,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::openFilesChanged);
|
&ChatRootView::openFilesChanged);
|
||||||
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().enableChatTools,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::useToolsChanged);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().enableThinkingMode,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::useThinkingChanged);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Settings::generalSettings().caProvider,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::isThinkingSupportChanged);
|
|
||||||
|
|
||||||
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
||||||
m_lastErrorMessage = error;
|
m_lastErrorMessage = error;
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
@@ -324,7 +298,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
if (m_pendingSend.active) {
|
if (m_pendingSend.active) {
|
||||||
PendingSend p = m_pendingSend;
|
PendingSend p = m_pendingSend;
|
||||||
m_pendingSend = {};
|
m_pendingSend = {};
|
||||||
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
dispatchSend(p.message, p.attachments, p.linkedFiles);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -337,7 +311,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
if (m_pendingSend.active) {
|
if (m_pendingSend.active) {
|
||||||
PendingSend p = m_pendingSend;
|
PendingSend p = m_pendingSend;
|
||||||
m_pendingSend = {};
|
m_pendingSend = {};
|
||||||
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
dispatchSend(p.message, p.attachments, p.linkedFiles);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -373,6 +347,57 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
|
|||||||
return m_skillsManager;
|
return m_skillsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AgentFactory *ChatRootView::agentFactory() const
|
||||||
|
{
|
||||||
|
if (!m_agentFactory) {
|
||||||
|
if (auto *engine = qmlEngine(this)) {
|
||||||
|
m_agentFactory = qobject_cast<AgentFactory *>(
|
||||||
|
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m_agentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionManager *ChatRootView::sessionManager() const
|
||||||
|
{
|
||||||
|
if (!m_sessionManager) {
|
||||||
|
if (auto *engine = qmlEngine(this)) {
|
||||||
|
m_sessionManager = qobject_cast<SessionManager *>(
|
||||||
|
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m_sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
QodeAssist::Session *ChatRootView::ownerSession()
|
||||||
|
{
|
||||||
|
if (!m_clientInterface)
|
||||||
|
return nullptr;
|
||||||
|
m_clientInterface->setSessionManager(sessionManager());
|
||||||
|
m_clientInterface->ensureSession();
|
||||||
|
return m_clientInterface->session();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::loadAvailableChatAgents()
|
||||||
|
{
|
||||||
|
m_agentController->setAgentFactory(agentFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatRootView::availableChatAgents() const
|
||||||
|
{
|
||||||
|
return m_agentController->availableAgents();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::currentChatAgent() const
|
||||||
|
{
|
||||||
|
return m_agentController->currentAgent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::setCurrentChatAgent(const QString &name)
|
||||||
|
{
|
||||||
|
m_agentController->setCurrentAgent(name);
|
||||||
|
}
|
||||||
|
|
||||||
QVariantList ChatRootView::searchSkills(const QString &query) const
|
QVariantList ChatRootView::searchSkills(const QString &query) const
|
||||||
{
|
{
|
||||||
QVariantList results;
|
QVariantList results;
|
||||||
@@ -380,7 +405,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
|
|||||||
if (!manager || !Settings::skillsSettings().enableSkills())
|
if (!manager || !Settings::skillsSettings().enableSkills())
|
||||||
return results;
|
return results;
|
||||||
|
|
||||||
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
QStringList projectSkillDirs;
|
QStringList projectSkillDirs;
|
||||||
if (project) {
|
if (project) {
|
||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
@@ -414,23 +439,22 @@ ChatModel *ChatRootView::chatModel() const
|
|||||||
|
|
||||||
void ChatRootView::sendMessage(const QString &message)
|
void ChatRootView::sendMessage(const QString &message)
|
||||||
{
|
{
|
||||||
const QStringList attachments = m_attachmentFiles;
|
if (message.trimmed().isEmpty() && m_attachmentFiles.isEmpty())
|
||||||
const QStringList linkedFiles = m_linkedFiles;
|
return;
|
||||||
const bool tools = useTools();
|
if (m_chatCompressor->isCompressing())
|
||||||
const bool thinking = useThinking();
|
|
||||||
|
|
||||||
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
dispatchSend(message, attachments, linkedFiles, tools, thinking);
|
const QStringList attachments = m_attachmentFiles;
|
||||||
|
const QStringList linkedFiles = m_linkedFiles;
|
||||||
|
|
||||||
|
if (deferSendForAutoCompress(message, attachments, linkedFiles))
|
||||||
|
return;
|
||||||
|
|
||||||
|
dispatchSend(message, attachments, linkedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::deferSendForAutoCompress(
|
bool ChatRootView::deferSendForAutoCompress(
|
||||||
const QString &message,
|
const QString &message, const QStringList &attachments, const QStringList &linkedFiles)
|
||||||
const QStringList &attachments,
|
|
||||||
const QStringList &linkedFiles,
|
|
||||||
bool useToolsArg,
|
|
||||||
bool useThinkingArg)
|
|
||||||
{
|
{
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
if (!settings.autoCompress())
|
if (!settings.autoCompress())
|
||||||
@@ -441,8 +465,14 @@ bool ChatRootView::deferSendForAutoCompress(
|
|||||||
if (inputTokens < threshold)
|
if (inputTokens < threshold)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (configuredCompressionAgent().isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
if (m_recentFilePath.isEmpty()) {
|
if (m_recentFilePath.isEmpty()) {
|
||||||
QString filePath = getAutosaveFilePath(message, attachments);
|
QString filePath = getAutosaveFilePath(message, attachments);
|
||||||
|
if (auto registry = sessionFileRegistry()) {
|
||||||
|
filePath = registry->uniqueFreePath(filePath);
|
||||||
|
}
|
||||||
if (filePath.isEmpty())
|
if (filePath.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
setRecentFilePath(filePath);
|
setRecentFilePath(filePath);
|
||||||
@@ -456,17 +486,13 @@ bool ChatRootView::deferSendForAutoCompress(
|
|||||||
.arg(inputTokens)
|
.arg(inputTokens)
|
||||||
.arg(threshold));
|
.arg(threshold));
|
||||||
|
|
||||||
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
|
m_pendingSend = {message, attachments, linkedFiles, true};
|
||||||
compressCurrentChat();
|
compressCurrentChat();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::dispatchSend(
|
void ChatRootView::dispatchSend(
|
||||||
const QString &message,
|
const QString &message, const QStringList &attachments, const QStringList &linkedFiles)
|
||||||
const QStringList &attachments,
|
|
||||||
const QStringList &linkedFiles,
|
|
||||||
bool useToolsArg,
|
|
||||||
bool useThinkingArg)
|
|
||||||
{
|
{
|
||||||
if (m_recentFilePath.isEmpty()) {
|
if (m_recentFilePath.isEmpty()) {
|
||||||
QString filePath = getAutosaveFilePath(message, attachments);
|
QString filePath = getAutosaveFilePath(message, attachments);
|
||||||
@@ -479,14 +505,16 @@ void ChatRootView::dispatchSend(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_tokenCounter->recordSent();
|
if (currentChatAgent().isEmpty())
|
||||||
|
loadAvailableChatAgents();
|
||||||
|
|
||||||
m_clientInterface->setSkillsManager(skillsManager());
|
m_clientInterface->setSkillsManager(skillsManager());
|
||||||
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
m_clientInterface->setSessionManager(sessionManager());
|
||||||
|
m_clientInterface->setActiveAgent(currentChatAgent());
|
||||||
|
m_clientInterface->sendMessage(message, attachments, linkedFiles);
|
||||||
|
|
||||||
m_fileManager->clearIntermediateStorage();
|
m_fileManager->clearIntermediateStorage();
|
||||||
clearAttachmentFiles();
|
clearAttachmentFiles();
|
||||||
setRequestProgressStatus(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::copyToClipboard(const QString &text)
|
void ChatRootView::copyToClipboard(const QString &text)
|
||||||
@@ -527,22 +555,15 @@ void ChatRootView::clearMessages()
|
|||||||
clearLinkedFiles();
|
clearLinkedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::currentTemplate() const
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
return settings.caModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::saveHistory(const QString &filePath)
|
void ChatRootView::saveHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (filePath != m_recentFilePath) {
|
if (auto registry = sessionFileRegistry();
|
||||||
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
|
registry && registry->isLockedByOther(filePath, ownerSession())) {
|
||||||
m_lastErrorMessage
|
m_lastErrorMessage = tr(
|
||||||
= tr("This chat file is already in use by another QodeAssist chat session.");
|
"This chat file is already in use by another QodeAssist chat session.");
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto result = m_historyStore->save(filePath);
|
auto result = m_historyStore->save(filePath);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -554,19 +575,24 @@ void ChatRootView::saveHistory(const QString &filePath)
|
|||||||
|
|
||||||
void ChatRootView::loadHistory(const QString &filePath)
|
void ChatRootView::loadHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (filePath != m_recentFilePath) {
|
if (auto registry = sessionFileRegistry();
|
||||||
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
|
registry && registry->isLockedByOther(filePath, ownerSession())) {
|
||||||
m_lastErrorMessage
|
m_lastErrorMessage = tr("This chat is already open in another QodeAssist chat session.");
|
||||||
= tr("This chat is already open in another QodeAssist chat session.");
|
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
m_clientInterface->cancelRequest();
|
||||||
|
|
||||||
auto result = m_historyStore->load(filePath);
|
auto result = m_historyStore->load(filePath);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
||||||
} else {
|
} else {
|
||||||
|
if (!result.errorMessage.isEmpty()) {
|
||||||
|
m_lastInfoMessage = result.errorMessage;
|
||||||
|
emit lastInfoMessageChanged();
|
||||||
|
LOG_MESSAGE(QString("Chat history loaded with issues: %1").arg(result.errorMessage));
|
||||||
|
}
|
||||||
setRecentFilePath(filePath);
|
setRecentFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,6 +612,12 @@ void ChatRootView::showSaveDialog()
|
|||||||
m_historyStore->showSaveDialog();
|
m_historyStore->showSaveDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatRootView::resetChatTo(int index)
|
||||||
|
{
|
||||||
|
m_clientInterface->cancelRequest();
|
||||||
|
m_chatModel->resetModelTo(index);
|
||||||
|
}
|
||||||
|
|
||||||
void ChatRootView::showLoadDialog()
|
void ChatRootView::showLoadDialog()
|
||||||
{
|
{
|
||||||
m_historyStore->showLoadDialog();
|
m_historyStore->showLoadDialog();
|
||||||
@@ -821,25 +853,6 @@ void ChatRootView::openChatHistoryFolder()
|
|||||||
m_historyStore->openHistoryFolder();
|
m_historyStore->openHistoryFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::openRulesFolder()
|
|
||||||
{
|
|
||||||
auto project = ProjectExplorer::ProjectManager::startupProject();
|
|
||||||
if (!project) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString projectPath = project->projectDirectory().toFSPathString();
|
|
||||||
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
|
||||||
|
|
||||||
QDir dir(rulesPath);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
dir.mkpath(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
|
||||||
QDesktopServices::openUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::openSettings()
|
void ChatRootView::openSettings()
|
||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
@@ -890,13 +903,12 @@ QString ChatRootView::chatTitle() const
|
|||||||
|
|
||||||
QString ChatRootView::computeChatTitle() const
|
QString ChatRootView::computeChatTitle() const
|
||||||
{
|
{
|
||||||
if (!m_chatModel)
|
if (!m_history)
|
||||||
return {};
|
return {};
|
||||||
const auto history = m_chatModel->getChatHistory();
|
for (const auto &msg : m_history->messages()) {
|
||||||
for (const auto &msg : history) {
|
if (msg.role() != Message::Role::User)
|
||||||
if (msg.role != ChatModel::User)
|
|
||||||
continue;
|
continue;
|
||||||
const QString content = msg.content.trimmed();
|
const QString content = msg.text().trimmed();
|
||||||
if (content.isEmpty())
|
if (content.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
|
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
|
||||||
@@ -954,8 +966,6 @@ void ChatRootView::relocateToWindow()
|
|||||||
clearAttachmentFiles();
|
clearAttachmentFiles();
|
||||||
emit closeHostRequested();
|
emit closeHostRequested();
|
||||||
|
|
||||||
// Closing the source split raises the main window; re-raise the chat window once that
|
|
||||||
// queued teardown has run. The registry outlives this view, which the split close deletes.
|
|
||||||
if (auto registry = sessionFileRegistry()) {
|
if (auto registry = sessionFileRegistry()) {
|
||||||
QMetaObject::invokeMethod(
|
QMetaObject::invokeMethod(
|
||||||
registry,
|
registry,
|
||||||
@@ -1052,7 +1062,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
registry->release(m_recentFilePath);
|
registry->release(m_recentFilePath);
|
||||||
}
|
}
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty()) {
|
||||||
registry->lock(filePath);
|
registry->lock(filePath, ownerSession());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,11 +1074,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
||||||
{
|
{
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
|
||||||
if (project
|
|
||||||
&& m_clientInterface->contextManager()
|
|
||||||
->ignoreManager()
|
|
||||||
->shouldIgnore(filePath.toFSPathString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
||||||
.arg(filePath.toFSPathString()));
|
.arg(filePath.toFSPathString()));
|
||||||
return true;
|
return true;
|
||||||
@@ -1120,71 +1126,9 @@ QString ChatRootView::lastErrorMessage() const
|
|||||||
return m_lastErrorMessage;
|
return m_lastErrorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList ChatRootView::activeRules() const
|
|
||||||
{
|
|
||||||
return m_activeRules;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::activeRulesCount() const
|
|
||||||
{
|
|
||||||
return m_activeRules.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::getRuleContent(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= m_activeRules.size())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
return PluginLLMCore::RulesLoader::loadRuleFileContent(
|
|
||||||
m_activeRules[index].toMap()["filePath"].toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::refreshRules()
|
|
||||||
{
|
|
||||||
m_activeRules.clear();
|
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (!project) {
|
|
||||||
emit activeRulesChanged();
|
|
||||||
emit activeRulesCountChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ruleFiles
|
|
||||||
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
|
|
||||||
|
|
||||||
for (const auto &ruleFile : ruleFiles) {
|
|
||||||
QVariantMap ruleMap;
|
|
||||||
ruleMap["filePath"] = ruleFile.filePath;
|
|
||||||
ruleMap["fileName"] = ruleFile.fileName;
|
|
||||||
ruleMap["category"] = ruleFile.category;
|
|
||||||
m_activeRules.append(ruleMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit activeRulesChanged();
|
|
||||||
emit activeRulesCountChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::useTools() const
|
bool ChatRootView::useTools() const
|
||||||
{
|
{
|
||||||
return Settings::chatAssistantSettings().enableChatTools();
|
return m_agentController->currentSupportsTools();
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::setUseTools(bool enabled)
|
|
||||||
{
|
|
||||||
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
|
|
||||||
Settings::chatAssistantSettings().writeSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::useThinking() const
|
|
||||||
{
|
|
||||||
return Settings::chatAssistantSettings().enableThinkingMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::setUseThinking(bool enabled)
|
|
||||||
{
|
|
||||||
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
|
|
||||||
Settings::chatAssistantSettings().writeSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::applyFileEdit(const QString &editId)
|
void ChatRootView::applyFileEdit(const QString &editId)
|
||||||
@@ -1217,11 +1161,6 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
|
|||||||
m_fileEditController->undoAllForCurrentMessage();
|
m_fileEditController->undoAllForCurrentMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::updateCurrentMessageEditsStats()
|
|
||||||
{
|
|
||||||
m_fileEditController->updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatRootView::currentMessageTotalEdits() const
|
int ChatRootView::currentMessageTotalEdits() const
|
||||||
{
|
{
|
||||||
return m_fileEditController->totalEdits();
|
return m_fileEditController->totalEdits();
|
||||||
@@ -1247,14 +1186,6 @@ QString ChatRootView::lastInfoMessage() const
|
|||||||
return m_lastInfoMessage;
|
return m_lastInfoMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::isThinkingSupport() const
|
|
||||||
{
|
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
|
|
||||||
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
||||||
{
|
{
|
||||||
for (const QString &filePath : attachments) {
|
for (const QString &filePath : attachments) {
|
||||||
@@ -1273,64 +1204,17 @@ bool ChatRootView::isImageFile(const QString &filePath) const
|
|||||||
return imageExtensions.contains(fileInfo.suffix().toLower());
|
return imageExtensions.contains(fileInfo.suffix().toLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::loadAvailableConfigurations()
|
QString ChatRootView::configuredCompressionAgent() const
|
||||||
{
|
{
|
||||||
m_configurationController->loadAvailableConfigurations();
|
const QString configured = Settings::PipelinesConfig::load().rosters.chatCompression;
|
||||||
|
if (!configured.isEmpty() && agentFactory() && agentFactory()->configByName(configured))
|
||||||
|
return configured;
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::applyConfiguration(const QString &configName)
|
bool ChatRootView::canCompress() const
|
||||||
{
|
{
|
||||||
m_configurationController->applyConfiguration(configName);
|
return !configuredCompressionAgent().isEmpty();
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChatRootView::availableConfigurations() const
|
|
||||||
{
|
|
||||||
return m_configurationController->availableConfigurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentConfiguration() const
|
|
||||||
{
|
|
||||||
return m_configurationController->currentConfiguration();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::loadAvailableAgentRoles()
|
|
||||||
{
|
|
||||||
m_agentRoleController->loadAvailableRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::applyAgentRole(const QString &roleName)
|
|
||||||
{
|
|
||||||
m_agentRoleController->applyRole(roleName);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChatRootView::availableAgentRoles() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->availableRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentAgentRole() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->currentRole();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::baseSystemPrompt() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->baseSystemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentAgentRoleDescription() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->currentRoleDescription();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
|
||||||
{
|
|
||||||
return m_agentRoleController->currentRoleSystemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::openAgentRolesSettings()
|
|
||||||
{
|
|
||||||
m_agentRoleController->openSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::compressCurrentChat()
|
void ChatRootView::compressCurrentChat()
|
||||||
@@ -1347,9 +1231,21 @@ void ChatRootView::compressCurrentChat()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
autosave();
|
const QString compressionAgent = configuredCompressionAgent();
|
||||||
|
if (compressionAgent.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
const auto saveResult = m_historyStore->save(m_recentFilePath);
|
||||||
|
if (!saveResult.success) {
|
||||||
|
m_lastErrorMessage
|
||||||
|
= tr("Failed to save chat before compression: %1").arg(saveResult.errorMessage);
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatCompressor->setSessionManager(sessionManager());
|
||||||
|
m_chatCompressor->setActiveAgent(compressionAgent);
|
||||||
|
m_chatCompressor->startCompression(m_recentFilePath, m_history);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::cancelCompression()
|
void ChatRootView::cancelCompression()
|
||||||
|
|||||||
@@ -11,18 +11,23 @@
|
|||||||
#include "ChatFileManager.hpp"
|
#include "ChatFileManager.hpp"
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
#include "pluginllmcore/PromptProviderChat.hpp"
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
namespace QodeAssist::Skills {
|
||||||
class SkillsManager;
|
class SkillsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
class SessionManager;
|
||||||
|
class ConversationHistory;
|
||||||
|
class Session;
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatCompressor;
|
class ChatCompressor;
|
||||||
class AgentRoleController;
|
class ChatAgentController;
|
||||||
class ChatConfigurationController;
|
|
||||||
class FileEditController;
|
class FileEditController;
|
||||||
class InputTokenCounter;
|
class InputTokenCounter;
|
||||||
class ChatHistoryStore;
|
class ChatHistoryStore;
|
||||||
@@ -32,7 +37,6 @@ class ChatRootView : public QQuickItem
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
|
||||||
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
|
||||||
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
|
||||||
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
|
||||||
@@ -46,25 +50,21 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
||||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
|
||||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
|
||||||
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
|
||||||
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
|
||||||
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
||||||
|
|
||||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
Q_PROPERTY(
|
||||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged
|
||||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
FINAL)
|
||||||
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
Q_PROPERTY(
|
||||||
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY
|
||||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
currentChatAgentChanged FINAL)
|
||||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
|
||||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
|
Q_PROPERTY(bool canCompress READ canCompress NOTIFY availableChatAgentsChanged FINAL)
|
||||||
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
||||||
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
||||||
|
|
||||||
@@ -75,13 +75,13 @@ public:
|
|||||||
~ChatRootView() override;
|
~ChatRootView() override;
|
||||||
|
|
||||||
ChatModel *chatModel() const;
|
ChatModel *chatModel() const;
|
||||||
QString currentTemplate() const;
|
|
||||||
|
|
||||||
void saveHistory(const QString &filePath);
|
void saveHistory(const QString &filePath);
|
||||||
void loadHistory(const QString &filePath);
|
void loadHistory(const QString &filePath);
|
||||||
|
|
||||||
Q_INVOKABLE void showSaveDialog();
|
Q_INVOKABLE void showSaveDialog();
|
||||||
Q_INVOKABLE void showLoadDialog();
|
Q_INVOKABLE void showLoadDialog();
|
||||||
|
Q_INVOKABLE void resetChatTo(int index);
|
||||||
|
|
||||||
void autosave();
|
void autosave();
|
||||||
QString getAutosaveFilePath() const;
|
QString getAutosaveFilePath() const;
|
||||||
@@ -104,7 +104,6 @@ public:
|
|||||||
QString sendShortcutText() const;
|
QString sendShortcutText() const;
|
||||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
Q_INVOKABLE void openChatHistoryFolder();
|
Q_INVOKABLE void openChatHistoryFolder();
|
||||||
Q_INVOKABLE void openRulesFolder();
|
|
||||||
Q_INVOKABLE void openSettings();
|
Q_INVOKABLE void openSettings();
|
||||||
|
|
||||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
@@ -140,17 +139,9 @@ public:
|
|||||||
|
|
||||||
QString lastErrorMessage() const;
|
QString lastErrorMessage() const;
|
||||||
|
|
||||||
QVariantList activeRules() const;
|
|
||||||
int activeRulesCount() const;
|
|
||||||
Q_INVOKABLE QString getRuleContent(int index);
|
|
||||||
Q_INVOKABLE void refreshRules();
|
|
||||||
|
|
||||||
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
||||||
|
|
||||||
bool useTools() const;
|
bool useTools() const;
|
||||||
void setUseTools(bool enabled);
|
|
||||||
bool useThinking() const;
|
|
||||||
void setUseThinking(bool enabled);
|
|
||||||
|
|
||||||
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
||||||
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
||||||
@@ -159,24 +150,14 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
|
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
|
||||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
|
||||||
|
|
||||||
Q_INVOKABLE void loadAvailableConfigurations();
|
|
||||||
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
|
||||||
QStringList availableConfigurations() const;
|
|
||||||
QString currentConfiguration() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void compressCurrentChat();
|
Q_INVOKABLE void compressCurrentChat();
|
||||||
Q_INVOKABLE void cancelCompression();
|
Q_INVOKABLE void cancelCompression();
|
||||||
|
|
||||||
Q_INVOKABLE void loadAvailableAgentRoles();
|
Q_INVOKABLE void loadAvailableChatAgents();
|
||||||
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
QStringList availableChatAgents() const;
|
||||||
Q_INVOKABLE void openAgentRolesSettings();
|
QString currentChatAgent() const;
|
||||||
QStringList availableAgentRoles() const;
|
void setCurrentChatAgent(const QString &name);
|
||||||
QString currentAgentRole() const;
|
|
||||||
QString baseSystemPrompt() const;
|
|
||||||
QString currentAgentRoleDescription() const;
|
|
||||||
QString currentAgentRoleSystemPrompt() const;
|
|
||||||
|
|
||||||
int currentMessageTotalEdits() const;
|
int currentMessageTotalEdits() const;
|
||||||
int currentMessageAppliedEdits() const;
|
int currentMessageAppliedEdits() const;
|
||||||
@@ -185,9 +166,8 @@ public:
|
|||||||
|
|
||||||
QString lastInfoMessage() const;
|
QString lastInfoMessage() const;
|
||||||
|
|
||||||
bool isThinkingSupport() const;
|
|
||||||
|
|
||||||
bool isCompressing() const;
|
bool isCompressing() const;
|
||||||
|
bool canCompress() const;
|
||||||
|
|
||||||
bool isInEditor() const;
|
bool isInEditor() const;
|
||||||
void setInEditor(bool value);
|
void setInEditor(bool value);
|
||||||
@@ -206,7 +186,6 @@ public slots:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void chatModelChanged();
|
void chatModelChanged();
|
||||||
void currentTemplateChanged();
|
|
||||||
void attachmentFilesChanged();
|
void attachmentFilesChanged();
|
||||||
void linkedFilesChanged();
|
void linkedFilesChanged();
|
||||||
void inputTokensCountChanged();
|
void inputTokensCountChanged();
|
||||||
@@ -223,20 +202,12 @@ signals:
|
|||||||
void lastErrorMessageChanged();
|
void lastErrorMessageChanged();
|
||||||
void lastInfoMessageChanged();
|
void lastInfoMessageChanged();
|
||||||
void sendShortcutTextChanged();
|
void sendShortcutTextChanged();
|
||||||
void activeRulesChanged();
|
|
||||||
void activeRulesCountChanged();
|
|
||||||
|
|
||||||
void useToolsChanged();
|
void useToolsChanged();
|
||||||
void useThinkingChanged();
|
|
||||||
void currentMessageEditsStatsChanged();
|
void currentMessageEditsStatsChanged();
|
||||||
|
|
||||||
void isThinkingSupportChanged();
|
void availableChatAgentsChanged();
|
||||||
void availableConfigurationsChanged();
|
void currentChatAgentChanged();
|
||||||
void currentConfigurationChanged();
|
|
||||||
|
|
||||||
void availableAgentRolesChanged();
|
|
||||||
void currentAgentRoleChanged();
|
|
||||||
void baseSystemPromptChanged();
|
|
||||||
|
|
||||||
void isCompressingChanged();
|
void isCompressingChanged();
|
||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
@@ -254,27 +225,22 @@ private:
|
|||||||
void triggerOpenChatCommand(Utils::Id commandId);
|
void triggerOpenChatCommand(Utils::Id commandId);
|
||||||
void handOffSession();
|
void handOffSession();
|
||||||
bool deferSendForAutoCompress(
|
bool deferSendForAutoCompress(
|
||||||
const QString &message,
|
const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
|
||||||
const QStringList &attachments,
|
|
||||||
const QStringList &linkedFiles,
|
|
||||||
bool useTools,
|
|
||||||
bool useThinking);
|
|
||||||
void dispatchSend(
|
void dispatchSend(
|
||||||
const QString &message,
|
const QString &message, const QStringList &attachments, const QStringList &linkedFiles);
|
||||||
const QStringList &attachments,
|
QString configuredCompressionAgent() const;
|
||||||
const QStringList &linkedFiles,
|
|
||||||
bool useTools,
|
|
||||||
bool useThinking);
|
|
||||||
bool hasImageAttachments(const QStringList &attachments) const;
|
bool hasImageAttachments(const QStringList &attachments) const;
|
||||||
|
|
||||||
SessionFileRegistry *sessionFileRegistry() const;
|
SessionFileRegistry *sessionFileRegistry() const;
|
||||||
Skills::SkillsManager *skillsManager() const;
|
Skills::SkillsManager *skillsManager() const;
|
||||||
|
AgentFactory *agentFactory() const;
|
||||||
|
SessionManager *sessionManager() const;
|
||||||
|
QodeAssist::Session *ownerSession();
|
||||||
|
|
||||||
|
QodeAssist::ConversationHistory *m_history;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
PluginLLMCore::PromptProviderChat m_promptProvider;
|
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
ChatFileManager *m_fileManager;
|
ChatFileManager *m_fileManager;
|
||||||
QString m_currentTemplate;
|
|
||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
QStringList m_linkedFiles;
|
QStringList m_linkedFiles;
|
||||||
@@ -283,8 +249,6 @@ private:
|
|||||||
QString message;
|
QString message;
|
||||||
QStringList attachments;
|
QStringList attachments;
|
||||||
QStringList linkedFiles;
|
QStringList linkedFiles;
|
||||||
bool useTools = false;
|
|
||||||
bool useThinking = false;
|
|
||||||
bool active = false;
|
bool active = false;
|
||||||
};
|
};
|
||||||
PendingSend m_pendingSend;
|
PendingSend m_pendingSend;
|
||||||
@@ -294,13 +258,11 @@ private:
|
|||||||
QList<Core::IEditor *> m_currentEditors;
|
QList<Core::IEditor *> m_currentEditors;
|
||||||
bool m_isRequestInProgress;
|
bool m_isRequestInProgress;
|
||||||
QString m_lastErrorMessage;
|
QString m_lastErrorMessage;
|
||||||
QVariantList m_activeRules;
|
|
||||||
|
|
||||||
QString m_lastInfoMessage;
|
QString m_lastInfoMessage;
|
||||||
|
|
||||||
ChatCompressor *m_chatCompressor;
|
ChatCompressor *m_chatCompressor;
|
||||||
AgentRoleController *m_agentRoleController;
|
ChatAgentController *m_agentController;
|
||||||
ChatConfigurationController *m_configurationController;
|
|
||||||
FileEditController *m_fileEditController;
|
FileEditController *m_fileEditController;
|
||||||
InputTokenCounter *m_tokenCounter;
|
InputTokenCounter *m_tokenCounter;
|
||||||
ChatHistoryStore *m_historyStore;
|
ChatHistoryStore *m_historyStore;
|
||||||
@@ -308,6 +270,8 @@ private:
|
|||||||
mutable bool m_sessionFileRegistryResolved = false;
|
mutable bool m_sessionFileRegistryResolved = false;
|
||||||
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
||||||
mutable bool m_skillsManagerResolved = false;
|
mutable bool m_skillsManagerResolved = false;
|
||||||
|
mutable QPointer<AgentFactory> m_agentFactory;
|
||||||
|
mutable QPointer<SessionManager> m_sessionManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <memory>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -13,12 +14,69 @@
|
|||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <MessageSerializer.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
const QString ChatSerializer::VERSION = "0.2";
|
namespace {
|
||||||
|
|
||||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
|
||||||
|
|
||||||
|
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
|
||||||
|
|
||||||
|
void registerEditFromResult(const QString &result)
|
||||||
{
|
{
|
||||||
|
const int pos = result.indexOf(kFileEditMarker);
|
||||||
|
if (pos < 0)
|
||||||
|
return;
|
||||||
|
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||||
|
if (!doc.isObject())
|
||||||
|
return;
|
||||||
|
const QJsonObject obj = doc.object();
|
||||||
|
const QString editId = obj.value("edit_id").toString();
|
||||||
|
const QString filePath = obj.value("file").toString();
|
||||||
|
if (editId.isEmpty() || filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
Context::ChangesManager::instance().addFileEdit(
|
||||||
|
editId,
|
||||||
|
filePath,
|
||||||
|
obj.value("old_content").toString(),
|
||||||
|
obj.value("new_content").toString(),
|
||||||
|
false,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendMergingAssistants(std::vector<Message> &out, Message message)
|
||||||
|
{
|
||||||
|
if (message.role() == Message::Role::Assistant && !out.empty()
|
||||||
|
&& out.back().role() == Message::Role::Assistant) {
|
||||||
|
if (out.back().id().isEmpty() && !message.id().isEmpty())
|
||||||
|
out.back().setId(message.id());
|
||||||
|
for (auto &block : message.takeBlocks())
|
||||||
|
out.back().appendBlock(std::move(block));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.push_back(std::move(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const QString ChatSerializer::VERSION = "0.3";
|
||||||
|
|
||||||
|
SerializationResult ChatSerializer::saveToFile(
|
||||||
|
const ConversationHistory *history, const QString &filePath, const QJsonObject &usage)
|
||||||
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
if (!ensureDirectoryExists(filePath)) {
|
if (!ensureDirectoryExists(filePath)) {
|
||||||
return {false, "Failed to create directory structure"};
|
return {false, "Failed to create directory structure"};
|
||||||
}
|
}
|
||||||
@@ -28,9 +86,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = serializeChat(model, filePath);
|
QJsonDocument doc(serializeChat(history, usage));
|
||||||
QJsonDocument doc(root);
|
|
||||||
|
|
||||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||||
}
|
}
|
||||||
@@ -38,8 +94,12 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {true, QString()};
|
return {true, QString()};
|
||||||
}
|
}
|
||||||
|
|
||||||
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
|
SerializationResult ChatSerializer::loadFromFile(
|
||||||
|
ConversationHistory *history, const QString &filePath, QJsonObject *usageOut)
|
||||||
{
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||||
@@ -51,180 +111,182 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
|||||||
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
return {false, QString("JSON parse error: %1").arg(error.errorString())};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = doc.object();
|
const QJsonObject root = doc.object();
|
||||||
QString version = root["version"].toString();
|
const QString version = root["version"].toString();
|
||||||
|
|
||||||
if (!validateVersion(version)) {
|
if (!validateVersion(version)) {
|
||||||
return {false, QString("Unsupported version: %1").arg(version)};
|
return {false, QString("Unsupported version: %1").arg(version)};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deserializeChat(model, root, filePath)) {
|
if (version == VERSION)
|
||||||
return {false, "Failed to deserialize chat data"};
|
return loadCurrent(history, root, usageOut);
|
||||||
}
|
return loadLegacy(history, root, usageOut);
|
||||||
|
|
||||||
return {true, QString()};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeMessage(
|
QJsonObject ChatSerializer::serializeChat(
|
||||||
const ChatModel::Message &message, const QString &chatFilePath)
|
const ConversationHistory *history, const QJsonObject &usage)
|
||||||
{
|
|
||||||
QJsonObject messageObj;
|
|
||||||
messageObj["role"] = static_cast<int>(message.role);
|
|
||||||
messageObj["content"] = message.content;
|
|
||||||
messageObj["id"] = message.id;
|
|
||||||
|
|
||||||
if (message.isRedacted) {
|
|
||||||
messageObj["isRedacted"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.signature.isEmpty()) {
|
|
||||||
messageObj["signature"] = message.signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
|
||||||
if (!message.toolName.isEmpty())
|
|
||||||
messageObj["toolName"] = message.toolName;
|
|
||||||
if (!message.toolArguments.isEmpty())
|
|
||||||
messageObj["toolArguments"] = message.toolArguments;
|
|
||||||
if (!message.toolResult.isEmpty())
|
|
||||||
messageObj["toolResult"] = message.toolResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.attachments.isEmpty()) {
|
|
||||||
QJsonArray attachmentsArray;
|
|
||||||
for (const auto &attachment : message.attachments) {
|
|
||||||
QJsonObject attachmentObj;
|
|
||||||
attachmentObj["fileName"] = attachment.filename;
|
|
||||||
attachmentObj["storedPath"] = attachment.content;
|
|
||||||
attachmentsArray.append(attachmentObj);
|
|
||||||
}
|
|
||||||
messageObj["attachments"] = attachmentsArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.images.isEmpty()) {
|
|
||||||
QJsonArray imagesArray;
|
|
||||||
for (const auto &image : message.images) {
|
|
||||||
QJsonObject imageObj;
|
|
||||||
imageObj["fileName"] = image.fileName;
|
|
||||||
imageObj["storedPath"] = image.storedPath;
|
|
||||||
imageObj["mediaType"] = image.mediaType;
|
|
||||||
imagesArray.append(imageObj);
|
|
||||||
}
|
|
||||||
messageObj["images"] = imagesArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.promptTokens > 0 || message.completionTokens > 0) {
|
|
||||||
QJsonObject usageObj;
|
|
||||||
usageObj["promptTokens"] = message.promptTokens;
|
|
||||||
usageObj["completionTokens"] = message.completionTokens;
|
|
||||||
if (message.cachedPromptTokens > 0)
|
|
||||||
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
|
|
||||||
if (message.reasoningTokens > 0)
|
|
||||||
usageObj["reasoningTokens"] = message.reasoningTokens;
|
|
||||||
messageObj["usage"] = usageObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatModel::Message ChatSerializer::deserializeMessage(
|
|
||||||
const QJsonObject &json, const QString &chatFilePath)
|
|
||||||
{
|
|
||||||
ChatModel::Message message;
|
|
||||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
|
||||||
message.content = json["content"].toString();
|
|
||||||
message.id = json["id"].toString();
|
|
||||||
message.isRedacted = json["isRedacted"].toBool(false);
|
|
||||||
message.signature = json["signature"].toString();
|
|
||||||
message.toolName = json["toolName"].toString();
|
|
||||||
message.toolArguments = json["toolArguments"].toObject();
|
|
||||||
message.toolResult = json["toolResult"].toString();
|
|
||||||
|
|
||||||
if (json.contains("attachments")) {
|
|
||||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
|
||||||
for (const auto &attachmentValue : attachmentsArray) {
|
|
||||||
QJsonObject attachmentObj = attachmentValue.toObject();
|
|
||||||
Context::ContentFile attachment;
|
|
||||||
attachment.filename = attachmentObj["fileName"].toString();
|
|
||||||
attachment.content = attachmentObj["storedPath"].toString();
|
|
||||||
message.attachments.append(attachment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("images")) {
|
|
||||||
QJsonArray imagesArray = json["images"].toArray();
|
|
||||||
for (const auto &imageValue : imagesArray) {
|
|
||||||
QJsonObject imageObj = imageValue.toObject();
|
|
||||||
ChatModel::ImageAttachment image;
|
|
||||||
image.fileName = imageObj["fileName"].toString();
|
|
||||||
image.storedPath = imageObj["storedPath"].toString();
|
|
||||||
image.mediaType = imageObj["mediaType"].toString();
|
|
||||||
message.images.append(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.contains("usage")) {
|
|
||||||
const QJsonObject usageObj = json["usage"].toObject();
|
|
||||||
message.promptTokens = usageObj["promptTokens"].toInt();
|
|
||||||
message.completionTokens = usageObj["completionTokens"].toInt();
|
|
||||||
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
|
|
||||||
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
|
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray;
|
QJsonArray messagesArray;
|
||||||
for (const auto &message : model->getChatHistory()) {
|
for (const auto &message : history->messages())
|
||||||
messagesArray.append(serializeMessage(message, chatFilePath));
|
messagesArray.append(MessageSerializer::toJson(message));
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
root["version"] = VERSION;
|
root["version"] = VERSION;
|
||||||
root["messages"] = messagesArray;
|
root["messages"] = messagesArray;
|
||||||
|
if (!usage.isEmpty())
|
||||||
|
root["usage"] = usage;
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::deserializeChat(
|
SerializationResult ChatSerializer::loadCurrent(
|
||||||
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut)
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray = json["messages"].toArray();
|
history->clear();
|
||||||
QVector<ChatModel::Message> messages;
|
|
||||||
messages.reserve(messagesArray.size());
|
|
||||||
|
|
||||||
for (const auto &messageValue : messagesArray) {
|
int skipped = 0;
|
||||||
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
|
const QJsonArray messagesArray = root["messages"].toArray();
|
||||||
|
for (const auto &value : messagesArray) {
|
||||||
|
bool ok = false;
|
||||||
|
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
|
||||||
|
if (ok)
|
||||||
|
history->append(std::move(message));
|
||||||
|
else
|
||||||
|
++skipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
model->clear();
|
if (usageOut)
|
||||||
|
*usageOut = root["usage"].toObject();
|
||||||
|
|
||||||
model->setLoadingFromHistory(true);
|
registerHistoricalFileEdits(history);
|
||||||
|
if (skipped > 0) {
|
||||||
for (const auto &message : messages) {
|
return {true, QString("%1 message(s) could not be parsed and were skipped").arg(skipped)};
|
||||||
model->addMessage(
|
|
||||||
message.content,
|
|
||||||
message.role,
|
|
||||||
message.id,
|
|
||||||
message.attachments,
|
|
||||||
message.images,
|
|
||||||
message.isRedacted,
|
|
||||||
message.signature);
|
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
|
||||||
model->setToolMessageData(
|
|
||||||
message.id, message.toolName, message.toolArguments, message.toolResult);
|
|
||||||
}
|
}
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
return {true, QString()};
|
||||||
.arg(message.images.size())
|
}
|
||||||
.arg(message.isRedacted)
|
|
||||||
.arg(message.signature.length()));
|
SerializationResult ChatSerializer::loadLegacy(
|
||||||
|
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut)
|
||||||
|
{
|
||||||
|
history->clear();
|
||||||
|
|
||||||
|
QJsonObject usage;
|
||||||
|
const auto collectUsage = [&usage](const QJsonObject &mj) {
|
||||||
|
const QString id = mj["id"].toString();
|
||||||
|
const QJsonObject legacyUsage = mj["usage"].toObject();
|
||||||
|
if (id.isEmpty() || legacyUsage.isEmpty())
|
||||||
|
return;
|
||||||
|
QJsonObject entry;
|
||||||
|
entry["prompt"] = legacyUsage["promptTokens"].toInt();
|
||||||
|
entry["completion"] = legacyUsage["completionTokens"].toInt();
|
||||||
|
entry["cached"] = legacyUsage["cachedPromptTokens"].toInt();
|
||||||
|
entry["reasoning"] = legacyUsage["reasoningTokens"].toInt();
|
||||||
|
usage.insert(id, entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Message> merged;
|
||||||
|
const QJsonArray arr = root["messages"].toArray();
|
||||||
|
int i = 0;
|
||||||
|
while (i < arr.size()) {
|
||||||
|
const QJsonObject mj = arr[i].toObject();
|
||||||
|
const auto role = static_cast<LegacyRole>(mj["role"].toInt());
|
||||||
|
|
||||||
|
if (role == LegacyRole::Tool) {
|
||||||
|
Message assistant(Message::Role::Assistant);
|
||||||
|
Message toolResults(Message::Role::User);
|
||||||
|
while (i < arr.size()
|
||||||
|
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt())
|
||||||
|
== LegacyRole::Tool) {
|
||||||
|
const QJsonObject tj = arr[i].toObject();
|
||||||
|
const QString toolName = tj["toolName"].toString();
|
||||||
|
const QString id = tj["id"].toString();
|
||||||
|
if (!toolName.isEmpty()) {
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::ToolUseContent>(
|
||||||
|
id, toolName, tj["toolArguments"].toObject()));
|
||||||
|
toolResults.appendBlock(
|
||||||
|
std::make_unique<LLMQore::ToolResultContent>(id, tj["toolResult"].toString()));
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (!assistant.blocks().empty()) {
|
||||||
|
appendMergingAssistants(merged, std::move(assistant));
|
||||||
|
merged.push_back(std::move(toolResults));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(false);
|
++i;
|
||||||
|
|
||||||
return true;
|
if (role == LegacyRole::FileEdit)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (role == LegacyRole::Thinking) {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
const QString signature = mj["signature"].toString();
|
||||||
|
Message assistant(Message::Role::Assistant);
|
||||||
|
if (mj["isRedacted"].toBool(false)) {
|
||||||
|
assistant.appendBlock(std::make_unique<LLMQore::RedactedThinkingContent>(signature));
|
||||||
|
} else {
|
||||||
|
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
|
||||||
|
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
|
||||||
|
}
|
||||||
|
appendMergingAssistants(merged, std::move(assistant));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == LegacyRole::User) {
|
||||||
|
Message user(Message::Role::User, mj["id"].toString());
|
||||||
|
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
|
||||||
|
for (const auto &a : mj["attachments"].toArray()) {
|
||||||
|
const QJsonObject ao = a.toObject();
|
||||||
|
user.appendBlock(
|
||||||
|
std::make_unique<StoredAttachmentContent>(
|
||||||
|
ao["fileName"].toString(), ao["storedPath"].toString()));
|
||||||
|
}
|
||||||
|
for (const auto &im : mj["images"].toArray()) {
|
||||||
|
const QJsonObject io = im.toObject();
|
||||||
|
user.appendBlock(
|
||||||
|
std::make_unique<StoredImageContent>(
|
||||||
|
io["fileName"].toString(),
|
||||||
|
io["storedPath"].toString(),
|
||||||
|
io["mediaType"].toString()));
|
||||||
|
}
|
||||||
|
merged.push_back(std::move(user));
|
||||||
|
} else {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
if (content.trimmed().isEmpty())
|
||||||
|
continue;
|
||||||
|
const Message::Role mapped = role == LegacyRole::System ? Message::Role::System
|
||||||
|
: Message::Role::Assistant;
|
||||||
|
Message message(mapped, mj["id"].toString());
|
||||||
|
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
|
||||||
|
collectUsage(mj);
|
||||||
|
if (mapped == Message::Role::Assistant)
|
||||||
|
appendMergingAssistants(merged, std::move(message));
|
||||||
|
else
|
||||||
|
merged.push_back(std::move(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &message : merged)
|
||||||
|
history->append(std::move(message));
|
||||||
|
|
||||||
|
if (usageOut)
|
||||||
|
*usageOut = usage;
|
||||||
|
|
||||||
|
registerHistoricalFileEdits(history);
|
||||||
|
return {true, QString()};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatSerializer::registerHistoricalFileEdits(const ConversationHistory *history)
|
||||||
|
{
|
||||||
|
for (const auto &message : history->messages()) {
|
||||||
|
for (const auto &block : message.blocks()) {
|
||||||
|
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
|
||||||
|
registerEditFromResult(tr->result());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
||||||
@@ -236,18 +298,7 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatSerializer::validateVersion(const QString &version)
|
bool ChatSerializer::validateVersion(const QString &version)
|
||||||
{
|
{
|
||||||
if (version == VERSION) {
|
return version == VERSION || version == "0.2" || version == "0.1";
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version == "0.1") {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
|
||||||
"to _content");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||||
@@ -303,10 +354,20 @@ bool ChatSerializer::saveContentToStorage(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath)
|
QString ChatSerializer::loadContentFromStorage(
|
||||||
|
const QString &chatFilePath, const QString &storedPath, StoredContentCache *cache)
|
||||||
{
|
{
|
||||||
QString contentFolder = getChatContentFolder(chatFilePath);
|
const QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
QString fullPath = QDir(contentFolder).filePath(storedPath);
|
const QString fullPath = QDir(contentFolder).filePath(storedPath);
|
||||||
|
|
||||||
|
const QFileInfo info(fullPath);
|
||||||
|
if (cache) {
|
||||||
|
const auto it = cache->constFind(fullPath);
|
||||||
|
if (it != cache->constEnd() && it->modified == info.lastModified()
|
||||||
|
&& it->size == info.size()) {
|
||||||
|
return it->base64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QFile file(fullPath);
|
QFile file(fullPath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
@@ -314,10 +375,12 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray contentData = file.readAll();
|
const QString base64 = QString::fromUtf8(file.readAll().toBase64());
|
||||||
file.close();
|
|
||||||
|
|
||||||
return contentData.toBase64();
|
if (cache)
|
||||||
|
cache->insert(fullPath, {info.lastModified(), info.size(), base64});
|
||||||
|
|
||||||
|
return base64;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QDateTime>
|
||||||
|
#include <QHash>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -18,29 +21,41 @@ struct SerializationResult
|
|||||||
QString errorMessage;
|
QString errorMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct StoredContentEntry
|
||||||
|
{
|
||||||
|
QDateTime modified;
|
||||||
|
qint64 size = 0;
|
||||||
|
QString base64;
|
||||||
|
};
|
||||||
|
|
||||||
|
using StoredContentCache = QHash<QString, StoredContentEntry>;
|
||||||
|
|
||||||
class ChatSerializer
|
class ChatSerializer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
|
static const QString VERSION;
|
||||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
|
||||||
|
|
||||||
// Public for testing purposes
|
static SerializationResult saveToFile(
|
||||||
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
|
const ConversationHistory *history, const QString &filePath, const QJsonObject &usage = {});
|
||||||
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
|
static SerializationResult loadFromFile(
|
||||||
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
ConversationHistory *history, const QString &filePath, QJsonObject *usageOut = nullptr);
|
||||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
|
||||||
|
|
||||||
// Content management (images and text files)
|
|
||||||
static QString getChatContentFolder(const QString &chatFilePath);
|
static QString getChatContentFolder(const QString &chatFilePath);
|
||||||
static bool saveContentToStorage(const QString &chatFilePath,
|
static bool saveContentToStorage(
|
||||||
|
const QString &chatFilePath,
|
||||||
const QString &fileName,
|
const QString &fileName,
|
||||||
const QString &base64Data,
|
const QString &base64Data,
|
||||||
QString &storedPath);
|
QString &storedPath);
|
||||||
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
static QString loadContentFromStorage(
|
||||||
|
const QString &chatFilePath, const QString &storedPath, StoredContentCache *cache = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString VERSION;
|
static QJsonObject serializeChat(const ConversationHistory *history, const QJsonObject &usage);
|
||||||
static constexpr int CURRENT_VERSION = 1;
|
static SerializationResult loadCurrent(
|
||||||
|
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
|
||||||
|
static SerializationResult loadLegacy(
|
||||||
|
ConversationHistory *history, const QJsonObject &root, QJsonObject *usageOut);
|
||||||
|
static void registerHistoricalFileEdits(const ConversationHistory *history);
|
||||||
|
|
||||||
static bool ensureDirectoryExists(const QString &filePath);
|
static bool ensureDirectoryExists(const QString &filePath);
|
||||||
static bool validateVersion(const QString &version);
|
static bool validateVersion(const QString &version);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,16 +5,25 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSet>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
#include <QStringList>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "Provider.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "pluginllmcore/IPromptProvider.hpp"
|
#include <ErrorInfo.hpp>
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <ResponseEvent.hpp>
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class SessionManager;
|
||||||
|
class Session;
|
||||||
|
class ConversationHistory;
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
namespace QodeAssist::Skills {
|
||||||
class SkillsManager;
|
class SkillsManager;
|
||||||
}
|
}
|
||||||
@@ -26,18 +35,18 @@ class ClientInterface : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ClientInterface(
|
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
||||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
|
||||||
~ClientInterface();
|
~ClientInterface();
|
||||||
|
|
||||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
||||||
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setHistory(ConversationHistory *history);
|
||||||
|
void setActiveAgent(const QString &agentName);
|
||||||
|
|
||||||
void sendMessage(
|
void sendMessage(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments = {},
|
const QList<QString> &attachments = {},
|
||||||
const QList<QString> &linkedFiles = {},
|
const QList<QString> &linkedFiles = {});
|
||||||
bool useTools = false,
|
|
||||||
bool useThinking = false);
|
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
@@ -46,57 +55,50 @@ public:
|
|||||||
void setChatFilePath(const QString &filePath);
|
void setChatFilePath(const QString &filePath);
|
||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
|
|
||||||
|
void ensureSession();
|
||||||
|
Session *session() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
void messageReceivedCompletely();
|
void messageReceivedCompletely();
|
||||||
void requestStarted(const QString &requestId);
|
void requestStarted(const QString &requestId);
|
||||||
|
void requestCancelled();
|
||||||
void messageUsageReceived(
|
void messageUsageReceived(
|
||||||
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
void handleThinkingBlockReceived(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
|
||||||
void handleToolExecutionStarted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &arguments);
|
|
||||||
void handleToolExecutionCompleted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &toolOutput);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
bool ensureAgentBound();
|
||||||
QString getCurrentFileContext() const;
|
|
||||||
QString getSystemPromptWithLinkedFiles(
|
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
|
||||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
void onSessionFinished(const QString &requestId);
|
||||||
|
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||||
|
void onSessionCancelled(const QString &requestId);
|
||||||
|
|
||||||
|
QStringList invokedSkillNames(const QString &message) const;
|
||||||
|
QString buildChatContextLayer() const;
|
||||||
|
QString requestIdForSession(Session *session) const;
|
||||||
bool isImageFile(const QString &filePath) const;
|
bool isImageFile(const QString &filePath) const;
|
||||||
QString getMediaTypeForImage(const QString &filePath) const;
|
QString getMediaTypeForImage(const QString &filePath) const;
|
||||||
QString encodeImageToBase64(const QString &filePath) const;
|
QString encodeImageToBase64(const QString &filePath) const;
|
||||||
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
|
||||||
|
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
PluginLLMCore::Provider *provider;
|
QPointer<Session> session;
|
||||||
bool dropPreToolText = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
|
QPointer<ConversationHistory> m_history;
|
||||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||||
|
QPointer<SessionManager> m_sessionManager;
|
||||||
|
QPointer<Session> m_session;
|
||||||
|
QString m_activeAgent;
|
||||||
|
QString m_boundAgent;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
|
std::shared_ptr<StoredContentCache> m_contentCache;
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
QHash<QString, QString> m_accumulatedResponses;
|
|
||||||
QSet<QString> m_awaitingContinuation;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -10,15 +10,13 @@
|
|||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "context/ChangesManager.h"
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
|
FileEditController::FileEditController(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
|
||||||
{
|
{
|
||||||
auto &changes = Context::ChangesManager::instance();
|
auto &changes = Context::ChangesManager::instance();
|
||||||
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
||||||
@@ -80,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit applied successfully"));
|
emit infoMessage(QString("File edit applied successfully"));
|
||||||
updateFileEditStatus(editId, "applied");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -95,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit rejected"));
|
emit infoMessage(QString("File edit rejected"));
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -110,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||||
emit infoMessage(QString("File edit undone successfully"));
|
emit infoMessage(QString("File edit undone successfully"));
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
emit errorOccurred(
|
emit errorOccurred(
|
||||||
@@ -163,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
|
|||||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
|
|
||||||
{
|
|
||||||
auto messages = m_chatModel->getChatHistory();
|
|
||||||
for (int i = 0; i < messages.size(); ++i) {
|
|
||||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
|
||||||
QString content = messages[i].content;
|
|
||||||
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
int markerPos = content.indexOf(marker);
|
|
||||||
|
|
||||||
QString jsonStr = content;
|
|
||||||
if (markerPos >= 0) {
|
|
||||||
jsonStr = content.mid(markerPos + marker.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
||||||
if (doc.isObject()) {
|
|
||||||
QJsonObject obj = doc.object();
|
|
||||||
obj["status"] = status;
|
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (!edit.statusMessage.isEmpty()) {
|
|
||||||
obj["status_message"] = edit.statusMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString updatedContent = marker
|
|
||||||
+ QString::fromUtf8(
|
|
||||||
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
|
||||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
|
||||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::applyAllForCurrentMessage()
|
void FileEditController::applyAllForCurrentMessage()
|
||||||
{
|
{
|
||||||
if (m_currentRequestId.isEmpty()) {
|
if (m_currentRequestId.isEmpty()) {
|
||||||
@@ -223,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
|
|||||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
|
|||||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,12 @@
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class FileEditController : public QObject
|
class FileEditController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit FileEditController(QObject *parent = nullptr);
|
||||||
|
|
||||||
void setCurrentRequestId(const QString &requestId);
|
void setCurrentRequestId(const QString &requestId);
|
||||||
void clearCurrentRequestId();
|
void clearCurrentRequestId();
|
||||||
@@ -41,9 +39,6 @@ signals:
|
|||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
QString m_currentRequestId;
|
QString m_currentRequestId;
|
||||||
int m_totalEdits{0};
|
int m_totalEdits{0};
|
||||||
int m_appliedEdits{0};
|
int m_appliedEdits{0};
|
||||||
|
|||||||
@@ -88,22 +88,6 @@ void FileMentionItem::moveDown()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileMentionItem::selectCurrent()
|
|
||||||
{
|
|
||||||
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
|
|
||||||
if (item.value("isProject").toBool()) {
|
|
||||||
emit projectSelected(item.value("projectName").toString());
|
|
||||||
} else {
|
|
||||||
emit fileSelected(
|
|
||||||
item.value("absolutePath").toString(),
|
|
||||||
item.value("relativePath").toString(),
|
|
||||||
item.value("projectName").toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileMentionItem::dismiss()
|
void FileMentionItem::dismiss()
|
||||||
{
|
{
|
||||||
m_searchResults.clear();
|
m_searchResults.clear();
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ public:
|
|||||||
Q_INVOKABLE void refreshSearch();
|
Q_INVOKABLE void refreshSearch();
|
||||||
Q_INVOKABLE void moveUp();
|
Q_INVOKABLE void moveUp();
|
||||||
Q_INVOKABLE void moveDown();
|
Q_INVOKABLE void moveDown();
|
||||||
Q_INVOKABLE void selectCurrent();
|
|
||||||
Q_INVOKABLE void dismiss();
|
Q_INVOKABLE void dismiss();
|
||||||
|
|
||||||
Q_INVOKABLE QVariantMap handleFileSelection(
|
Q_INVOKABLE QVariantMap handleFileSelection(
|
||||||
|
|||||||
@@ -4,50 +4,20 @@
|
|||||||
|
|
||||||
#include "InputTokenCounter.hpp"
|
#include "InputTokenCounter.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
#include "Logger.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
#include "context/TokenUtils.hpp"
|
#include "context/TokenUtils.hpp"
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
InputTokenCounter::InputTokenCounter(
|
InputTokenCounter::InputTokenCounter(
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
|
ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_history(history)
|
||||||
, m_contextManager(contextManager)
|
, m_contextManager(contextManager)
|
||||||
{
|
{
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
|
||||||
connect(
|
|
||||||
&settings.useSystemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&InputTokenCounter::recompute);
|
|
||||||
connect(
|
|
||||||
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
|
|
||||||
connect(
|
|
||||||
&settings.enableChatTools,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&InputTokenCounter::recompute);
|
|
||||||
|
|
||||||
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
|
||||||
rewireToolsChangedConnection();
|
|
||||||
recompute();
|
|
||||||
});
|
|
||||||
|
|
||||||
rewireToolsChangedConnection();
|
|
||||||
recompute();
|
recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,33 +44,8 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
|||||||
recompute();
|
recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputTokenCounter::rewireToolsChangedConnection()
|
|
||||||
{
|
|
||||||
if (m_toolsChangedConn)
|
|
||||||
QObject::disconnect(m_toolsChangedConn);
|
|
||||||
m_toolsChangedConn = {};
|
|
||||||
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
if (!provider)
|
|
||||||
return;
|
|
||||||
auto *tm = provider->toolsManager();
|
|
||||||
if (!tm)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_toolsChangedConn = connect(
|
|
||||||
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::recompute()
|
void InputTokenCounter::recompute()
|
||||||
{
|
{
|
||||||
int inputTokens = m_messageTokens;
|
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
|
||||||
|
|
||||||
if (settings.useSystemPrompt()) {
|
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
|
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
|
||||||
int imageTokens = 0;
|
int imageTokens = 0;
|
||||||
for (const QString &p : paths) {
|
for (const QString &p : paths) {
|
||||||
@@ -112,15 +57,24 @@ void InputTokenCounter::recompute()
|
|||||||
return imageTokens;
|
return imageTokens;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
int pendingTokens = m_messageTokens;
|
||||||
if (!m_attachments.isEmpty()) {
|
if (!m_attachments.isEmpty()) {
|
||||||
QStringList textPaths;
|
QStringList textPaths;
|
||||||
inputTokens += splitImageEstimate(m_attachments, textPaths);
|
pendingTokens += splitImageEstimate(m_attachments, textPaths);
|
||||||
if (!textPaths.isEmpty()) {
|
if (!textPaths.isEmpty()) {
|
||||||
auto attachFiles = m_contextManager->getContentFiles(textPaths);
|
auto attachFiles = m_contextManager->getContentFiles(textPaths);
|
||||||
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
pendingTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_hasServerUsage && m_history && !m_history->isEmpty()) {
|
||||||
|
m_inputTokens = m_serverInputTokens + pendingTokens;
|
||||||
|
emit inputTokensChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int inputTokens = pendingTokens;
|
||||||
|
|
||||||
if (!m_linkedFiles.isEmpty()) {
|
if (!m_linkedFiles.isEmpty()) {
|
||||||
QStringList textPaths;
|
QStringList textPaths;
|
||||||
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
|
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
|
||||||
@@ -130,54 +84,32 @@ void InputTokenCounter::recompute()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto &history = m_chatModel->getChatHistory();
|
if (m_history) {
|
||||||
for (const auto &message : history) {
|
for (const auto &message : m_history->messages()) {
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
inputTokens += Context::TokenUtils::estimateTokens(message.text());
|
||||||
inputTokens += 4; // + role
|
inputTokens += 4;
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.enableChatTools()) {
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
|
||||||
providerName)) {
|
|
||||||
if (auto *tm = provider->toolsManager()) {
|
|
||||||
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
|
||||||
if (!toolDefs.isEmpty()) {
|
|
||||||
const QByteArray serialized
|
|
||||||
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
|
||||||
inputTokens += static_cast<int>(serialized.size() / 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
m_inputTokens = inputTokens;
|
||||||
emit inputTokensChanged();
|
emit inputTokensChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputTokenCounter::recordSent()
|
void InputTokenCounter::recordServerUsage(int promptTokens, int cachedTokens)
|
||||||
{
|
{
|
||||||
m_lastSentEstimate = m_calibrationFactor > 0.0
|
const int serverInput = promptTokens + cachedTokens;
|
||||||
? static_cast<int>(m_inputTokens / m_calibrationFactor)
|
if (serverInput <= 0)
|
||||||
: m_inputTokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::recordServerUsage(int promptTokens)
|
|
||||||
{
|
|
||||||
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const double rawFactor
|
m_serverInputTokens = serverInput;
|
||||||
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
|
m_hasServerUsage = true;
|
||||||
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
|
recompute();
|
||||||
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped;
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
|
|
||||||
.arg(promptTokens)
|
|
||||||
.arg(m_lastSentEstimate)
|
|
||||||
.arg(rawFactor, 0, 'f', 3)
|
|
||||||
.arg(m_calibrationFactor, 0, 'f', 3));
|
|
||||||
|
|
||||||
|
void InputTokenCounter::resetServerUsage()
|
||||||
|
{
|
||||||
|
m_serverInputTokens = 0;
|
||||||
|
m_hasServerUsage = false;
|
||||||
recompute();
|
recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,25 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
class ContextManager;
|
class ContextManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class InputTokenCounter : public QObject
|
class InputTokenCounter : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
InputTokenCounter(
|
InputTokenCounter(
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr);
|
ConversationHistory *history,
|
||||||
|
Context::ContextManager *contextManager,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
int inputTokens() const;
|
int inputTokens() const;
|
||||||
|
|
||||||
@@ -30,25 +34,22 @@ public:
|
|||||||
void setLinkedFiles(const QStringList &linkedFiles);
|
void setLinkedFiles(const QStringList &linkedFiles);
|
||||||
void recompute();
|
void recompute();
|
||||||
|
|
||||||
void recordSent();
|
void recordServerUsage(int promptTokens, int cachedTokens);
|
||||||
void recordServerUsage(int promptTokens);
|
void resetServerUsage();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void inputTokensChanged();
|
void inputTokensChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void rewireToolsChangedConnection();
|
ConversationHistory *m_history;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
QMetaObject::Connection m_toolsChangedConn;
|
|
||||||
|
|
||||||
QStringList m_attachments;
|
QStringList m_attachments;
|
||||||
QStringList m_linkedFiles;
|
QStringList m_linkedFiles;
|
||||||
int m_messageTokens{0};
|
int m_messageTokens{0};
|
||||||
int m_inputTokens{0};
|
int m_inputTokens{0};
|
||||||
int m_lastSentEstimate{0};
|
int m_serverInputTokens{0};
|
||||||
double m_calibrationFactor{1.0};
|
bool m_hasServerUsage{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -8,29 +8,43 @@
|
|||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
#include <Session.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
SessionFileRegistry::~SessionFileRegistry() = default;
|
||||||
|
|
||||||
bool SessionFileRegistry::isLocked(const QString &path) const
|
bool SessionFileRegistry::isLocked(const QString &path) const
|
||||||
{
|
{
|
||||||
return !path.isEmpty() && m_lockedPaths.contains(path);
|
return !path.isEmpty() && !m_locks.value(path).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SessionFileRegistry::lock(const QString &path)
|
bool SessionFileRegistry::isLockedByOther(const QString &path, QodeAssist::Session *self) const
|
||||||
{
|
{
|
||||||
if (path.isEmpty() || m_lockedPaths.contains(path)) {
|
if (path.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
}
|
const auto owner = m_locks.value(path);
|
||||||
m_lockedPaths.insert(path);
|
return !owner.isNull() && owner != self;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SessionFileRegistry::lock(const QString &path, QodeAssist::Session *owner)
|
||||||
|
{
|
||||||
|
if (path.isEmpty())
|
||||||
|
return false;
|
||||||
|
const auto existing = m_locks.value(path);
|
||||||
|
if (!existing.isNull() && existing != owner)
|
||||||
|
return false;
|
||||||
|
m_locks.insert(path, owner);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionFileRegistry::release(const QString &path)
|
void SessionFileRegistry::release(const QString &path)
|
||||||
{
|
{
|
||||||
m_lockedPaths.remove(path);
|
m_locks.remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
||||||
@@ -45,7 +59,7 @@ QString SessionFileRegistry::takePendingChatFile()
|
|||||||
|
|
||||||
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
||||||
{
|
{
|
||||||
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
|
if (desiredPath.isEmpty() || !isLocked(desiredPath)) {
|
||||||
return desiredPath;
|
return desiredPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +73,7 @@ QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
|||||||
if (!suffix.isEmpty()) {
|
if (!suffix.isEmpty()) {
|
||||||
candidate += '.' + suffix;
|
candidate += '.' + suffix;
|
||||||
}
|
}
|
||||||
if (!m_lockedPaths.contains(candidate)) {
|
if (!isLocked(candidate)) {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,35 +4,37 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSet>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class Session;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
// Shared registry of chat session (autosave) file paths that are currently held by a live
|
|
||||||
// chat instance. Lets every chat view — bottom pane, navigation panel, editor split — claim
|
|
||||||
// a unique history file so two sessions never autosave into the same path.
|
|
||||||
class SessionFileRegistry : public QObject
|
class SessionFileRegistry : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit SessionFileRegistry(QObject *parent = nullptr);
|
explicit SessionFileRegistry(QObject *parent = nullptr);
|
||||||
|
~SessionFileRegistry() override;
|
||||||
|
|
||||||
bool isLocked(const QString &path) const;
|
bool isLocked(const QString &path) const;
|
||||||
bool lock(const QString &path);
|
bool isLockedByOther(const QString &path, QodeAssist::Session *self) const;
|
||||||
|
bool lock(const QString &path, QodeAssist::Session *owner);
|
||||||
void release(const QString &path);
|
void release(const QString &path);
|
||||||
|
|
||||||
QString uniqueFreePath(const QString &desiredPath) const;
|
QString uniqueFreePath(const QString &desiredPath) const;
|
||||||
|
|
||||||
// Handoff slot for relocating a live chat between hosts (split <-> window): the source
|
|
||||||
// chat stores its history file here, the freshly created host picks it up exactly once.
|
|
||||||
void setPendingChatFile(const QString &path);
|
void setPendingChatFile(const QString &path);
|
||||||
QString takePendingChatFile();
|
QString takePendingChatFile();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSet<QString> m_lockedPaths;
|
QHash<QString, QPointer<QodeAssist::Session>> m_locks;
|
||||||
QString m_pendingChatFile;
|
QString m_pendingChatFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
@@ -118,7 +118,6 @@ ChatRootView {
|
|||||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||||
}
|
}
|
||||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||||
contextButton.onClicked: contextViewer.open()
|
|
||||||
pinButton {
|
pinButton {
|
||||||
visible: typeof _chatview !== 'undefined'
|
visible: typeof _chatview !== 'undefined'
|
||||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||||
@@ -138,43 +137,18 @@ ChatRootView {
|
|||||||
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
||||||
? qsTr("Move this chat to an editor tab")
|
? qsTr("Move this chat to an editor tab")
|
||||||
: qsTr("Move this chat to a separate window")
|
: qsTr("Move this chat to a separate window")
|
||||||
toolsButton {
|
|
||||||
checked: root.useTools
|
|
||||||
onCheckedChanged: {
|
|
||||||
root.useTools = toolsButton.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thinkingMode {
|
|
||||||
checked: root.useThinking
|
|
||||||
enabled: root.isThinkingSupport
|
|
||||||
onCheckedChanged: {
|
|
||||||
root.useThinking = thinkingMode.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsButton.onClicked: root.openSettings()
|
settingsButton.onClicked: root.openSettings()
|
||||||
configSelector {
|
agentSelector {
|
||||||
model: root.availableConfigurations
|
model: root.availableChatAgents
|
||||||
displayText: root.currentConfiguration
|
displayText: root.currentChatAgent
|
||||||
onActivated: function(index) {
|
onActivated: function(index) {
|
||||||
if (index > 0) {
|
root.currentChatAgent = root.availableChatAgents[index]
|
||||||
root.applyConfiguration(root.availableConfigurations[index])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: root.loadAvailableChatAgents()
|
||||||
|
|
||||||
popup.onAboutToShow: {
|
popup.onAboutToShow: {
|
||||||
root.loadAvailableConfigurations()
|
root.loadAvailableChatAgents()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
roleSelector {
|
|
||||||
model: root.availableAgentRoles
|
|
||||||
displayText: root.currentAgentRole
|
|
||||||
onActivated: function(index) {
|
|
||||||
root.applyAgentRole(root.availableAgentRoles[index])
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.onAboutToShow: {
|
|
||||||
root.loadAvailableAgentRoles()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +317,7 @@ ChatRootView {
|
|||||||
onResetChatToMessage: function(idx) {
|
onResetChatToMessage: function(idx) {
|
||||||
messageInput.text = model.content
|
messageInput.text = model.content
|
||||||
messageInput.cursorPosition = model.content.length
|
messageInput.cursorPosition = model.content.length
|
||||||
root.chatModel.resetModelTo(idx)
|
root.resetChatTo(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpenFileRequested: function(filePath) {
|
onOpenFileRequested: function(filePath) {
|
||||||
@@ -593,6 +567,8 @@ ChatRootView {
|
|||||||
|
|
||||||
isCompressing: root.isCompressing
|
isCompressing: root.isCompressing
|
||||||
isProcessing: root.isRequestInProgress
|
isProcessing: root.isRequestInProgress
|
||||||
|
canCompress: root.canCompress
|
||||||
|
canSend: root.currentChatAgent !== ""
|
||||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||||
: root.cancelRequest()
|
: root.cancelRequest()
|
||||||
sendButton.icon.source: root.isRequestInProgress
|
sendButton.icon.source: root.isRequestInProgress
|
||||||
@@ -604,9 +580,11 @@ ChatRootView {
|
|||||||
? root.errorColor : "transparent"
|
? root.errorColor : "transparent"
|
||||||
sendButtonTooltip.text: root.isRequestInProgress
|
sendButtonTooltip.text: root.isRequestInProgress
|
||||||
? qsTr("Stop")
|
? qsTr("Stop")
|
||||||
|
: (root.currentChatAgent === ""
|
||||||
|
? qsTr("Assign a chat agent in the Pipelines settings")
|
||||||
: (root.hasActiveError
|
: (root.hasActiveError
|
||||||
? root.lastErrorMessage
|
? root.lastErrorMessage
|
||||||
: qsTr("Send message to LLM %1").arg(root.sendShortcutText))
|
: qsTr("Send message to LLM %1").arg(root.sendShortcutText)))
|
||||||
compressButton.onClicked: compressConfirmDialog.open()
|
compressButton.onClicked: compressConfirmDialog.open()
|
||||||
cancelCompressButton.onClicked: root.cancelCompression()
|
cancelCompressButton.onClicked: root.cancelCompression()
|
||||||
syncOpenFiles {
|
syncOpenFiles {
|
||||||
@@ -681,6 +659,10 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
|
if (root.isCompressing)
|
||||||
|
return
|
||||||
|
if (messageInput.text.trim() === "" && root.attachmentFiles.length === 0)
|
||||||
|
return
|
||||||
root.hasActiveError = false
|
root.hasActiveError = false
|
||||||
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
@@ -831,30 +813,6 @@ ChatRootView {
|
|||||||
toastTextColor: "#FFFFFF"
|
toastTextColor: "#FFFFFF"
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextViewer {
|
|
||||||
id: contextViewer
|
|
||||||
|
|
||||||
width: Math.min(parent.width * 0.85, 800)
|
|
||||||
height: Math.min(parent.height * 0.85, 700)
|
|
||||||
x: (parent.width - width) / 2
|
|
||||||
y: (parent.height - height) / 2
|
|
||||||
|
|
||||||
baseSystemPrompt: root.baseSystemPrompt
|
|
||||||
currentAgentRole: root.currentAgentRole
|
|
||||||
currentAgentRoleDescription: root.currentAgentRoleDescription
|
|
||||||
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
|
||||||
activeRules: root.activeRules
|
|
||||||
activeRulesCount: root.activeRulesCount
|
|
||||||
|
|
||||||
onOpenSettings: root.openSettings()
|
|
||||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
|
||||||
onOpenRulesFolder: root.openRulesFolder()
|
|
||||||
onRefreshRules: root.refreshRules()
|
|
||||||
onRuleSelected: function(index) {
|
|
||||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onLastErrorMessageChanged() {
|
function onLastErrorMessageChanged() {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ Rectangle {
|
|||||||
|
|
||||||
property bool isCompressing: false
|
property bool isCompressing: false
|
||||||
property bool isProcessing: false
|
property bool isProcessing: false
|
||||||
|
property bool canCompress: true
|
||||||
|
property bool canSend: true
|
||||||
property alias sendButtonTooltip: sendButtonTooltipId
|
property alias sendButtonTooltip: sendButtonTooltipId
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
@@ -139,10 +141,18 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: compressButtonContainer
|
||||||
|
|
||||||
|
visible: !root.isCompressing
|
||||||
|
implicitWidth: compressButtonId.implicitWidth
|
||||||
|
implicitHeight: compressButtonId.implicitHeight
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: compressButtonId
|
id: compressButtonId
|
||||||
|
|
||||||
visible: !root.isCompressing
|
anchors.fill: parent
|
||||||
|
enabled: root.canCompress
|
||||||
text: qsTr("Compress")
|
text: qsTr("Compress")
|
||||||
|
|
||||||
icon {
|
icon {
|
||||||
@@ -150,17 +160,32 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: compressHoverHandler
|
||||||
|
}
|
||||||
|
|
||||||
QoAToolTip {
|
QoAToolTip {
|
||||||
visible: compressButtonId.hovered
|
visible: compressHoverHandler.hovered
|
||||||
delay: 250
|
delay: 250
|
||||||
text: qsTr("Compress chat (create summarized copy using LLM)")
|
text: root.canCompress
|
||||||
|
? qsTr("Compress chat (create summarized copy using LLM)")
|
||||||
|
: qsTr("Assign a compression agent in the Pipelines settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: sendButtonContainer
|
||||||
|
|
||||||
|
implicitWidth: sendButtonId.implicitWidth
|
||||||
|
implicitHeight: sendButtonId.implicitHeight
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: sendButtonId
|
id: sendButtonId
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.isProcessing || (root.canSend && !root.isCompressing)
|
||||||
leftPadding: root.isProcessing ? 22 : 4
|
leftPadding: root.isProcessing ? 22 : 4
|
||||||
|
|
||||||
icon {
|
icon {
|
||||||
@@ -178,11 +203,16 @@ Rectangle {
|
|||||||
height: 14
|
height: 14
|
||||||
running: root.isProcessing
|
running: root.isProcessing
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: sendHoverHandler
|
||||||
|
}
|
||||||
|
|
||||||
QoAToolTip {
|
QoAToolTip {
|
||||||
id: sendButtonTooltipId
|
id: sendButtonTooltipId
|
||||||
|
|
||||||
visible: sendButtonId.hovered
|
visible: sendHoverHandler.hovered
|
||||||
delay: 250
|
delay: 250
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,543 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls.Basic as QQC
|
|
||||||
|
|
||||||
import UIControls
|
|
||||||
import ChatView
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string baseSystemPrompt
|
|
||||||
property string currentAgentRole
|
|
||||||
property string currentAgentRoleDescription
|
|
||||||
property string currentAgentRoleSystemPrompt
|
|
||||||
property var activeRules
|
|
||||||
property int activeRulesCount
|
|
||||||
property string selectedRuleContent
|
|
||||||
|
|
||||||
signal openSettings()
|
|
||||||
signal openAgentRolesSettings()
|
|
||||||
signal openRulesFolder()
|
|
||||||
signal refreshRules()
|
|
||||||
signal ruleSelected(int index)
|
|
||||||
|
|
||||||
modal: true
|
|
||||||
focus: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: palette.window
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatUtils {
|
|
||||||
id: utils
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Chat Context")
|
|
||||||
font.pixelSize: 16
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Refresh")
|
|
||||||
onClicked: root.refreshRules()
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Close")
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
id: mainFlickable
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
contentHeight: sectionsColumn.implicitHeight
|
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: sectionsColumn
|
|
||||||
|
|
||||||
width: mainFlickable.width
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
CollapsibleSection {
|
|
||||||
id: systemPromptSection
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
title: qsTr("Base System Prompt")
|
|
||||||
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
|
|
||||||
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
|
|
||||||
|
|
||||||
sectionContent: ColumnLayout {
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
id: systemPromptFlickable
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
contentHeight: systemPromptText.implicitHeight
|
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: systemPromptText
|
|
||||||
|
|
||||||
width: systemPromptFlickable.width
|
|
||||||
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
|
|
||||||
font.family: "monospace"
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
|
||||||
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: root.baseSystemPrompt.length > 0
|
|
||||||
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Edit in Settings")
|
|
||||||
onClicked: {
|
|
||||||
root.openSettings()
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CollapsibleSection {
|
|
||||||
id: agentRoleSection
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
title: qsTr("Agent Role")
|
|
||||||
badge: root.currentAgentRole
|
|
||||||
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
|
|
||||||
|
|
||||||
sectionContent: ColumnLayout {
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.currentAgentRoleDescription
|
|
||||||
font.pixelSize: 11
|
|
||||||
font.italic: true
|
|
||||||
color: palette.mid
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: root.currentAgentRoleDescription.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
visible: root.currentAgentRoleSystemPrompt.length > 0
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
id: agentPromptFlickable
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
contentHeight: agentPromptText.implicitHeight
|
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: agentPromptText
|
|
||||||
|
|
||||||
width: agentPromptFlickable.width
|
|
||||||
text: root.currentAgentRoleSystemPrompt
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
color: palette.text
|
|
||||||
font.family: "monospace"
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
|
||||||
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("No role selected. Using base system prompt only.")
|
|
||||||
font.pixelSize: 11
|
|
||||||
color: palette.mid
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: root.currentAgentRoleSystemPrompt.length === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: root.currentAgentRoleSystemPrompt.length > 0
|
|
||||||
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Manage Roles")
|
|
||||||
onClicked: {
|
|
||||||
root.openAgentRolesSettings()
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CollapsibleSection {
|
|
||||||
id: projectRulesSection
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
title: qsTr("Project Rules")
|
|
||||||
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
|
|
||||||
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
|
|
||||||
|
|
||||||
sectionContent: ColumnLayout {
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
SplitView {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 220
|
|
||||||
orientation: Qt.Horizontal
|
|
||||||
visible: root.activeRulesCount > 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
SplitView.minimumWidth: 120
|
|
||||||
SplitView.preferredWidth: 180
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Rules (%1)").arg(rulesList.count)
|
|
||||||
font.pixelSize: 11
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: rulesList
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
clip: true
|
|
||||||
model: root.activeRules
|
|
||||||
currentIndex: 0
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
height: ruleItemContent.implicitHeight + 8
|
|
||||||
highlighted: ListView.isCurrentItem
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: {
|
|
||||||
if (parent.highlighted)
|
|
||||||
return palette.highlight
|
|
||||||
if (parent.hovered)
|
|
||||||
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
radius: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
id: ruleItemContent
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: modelData.fileName
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: modelData.category
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
rulesList.currentIndex = index
|
|
||||||
root.ruleSelected(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
|
||||||
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
SplitView.fillWidth: true
|
|
||||||
SplitView.minimumWidth: 200
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Content")
|
|
||||||
font.pixelSize: 11
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: root.selectedRuleContent.length > 0
|
|
||||||
onClicked: utils.copyToClipboard(root.selectedRuleContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
id: ruleContentFlickable
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
contentHeight: ruleContentArea.implicitHeight
|
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: ruleContentArea
|
|
||||||
|
|
||||||
width: ruleContentFlickable.width
|
|
||||||
text: root.selectedRuleContent
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
selectionColor: palette.highlight
|
|
||||||
color: palette.text
|
|
||||||
font.family: "monospace"
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
|
||||||
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
|
|
||||||
font.pixelSize: 11
|
|
||||||
color: palette.mid
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: root.activeRulesCount === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Open Rules Folder")
|
|
||||||
onClicked: root.openRulesFolder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
|
||||||
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: palette.mid
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component CollapsibleSection: ColumnLayout {
|
|
||||||
id: sectionRoot
|
|
||||||
|
|
||||||
property string title
|
|
||||||
property string badge
|
|
||||||
property color badgeColor: palette.mid
|
|
||||||
property Component sectionContent: null
|
|
||||||
property bool expanded: false
|
|
||||||
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 32
|
|
||||||
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: sectionMouseArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: sectionRoot.expanded = !sectionRoot.expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 8
|
|
||||||
anchors.rightMargin: 8
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: sectionRoot.expanded ? "▼" : "▶"
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: sectionRoot.title
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
implicitWidth: badgeText.implicitWidth + 12
|
|
||||||
implicitHeight: 18
|
|
||||||
color: sectionRoot.badgeColor
|
|
||||||
radius: 3
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: badgeText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: sectionRoot.badge
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: "#FFFFFF"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 12
|
|
||||||
Layout.topMargin: 8
|
|
||||||
Layout.bottomMargin: 4
|
|
||||||
sourceComponent: sectionRoot.sectionContent
|
|
||||||
visible: sectionRoot.expanded
|
|
||||||
active: sectionRoot.expanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpened: {
|
|
||||||
if (root.activeRulesCount > 0) {
|
|
||||||
root.ruleSelected(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,12 +22,8 @@ Rectangle {
|
|||||||
property alias openChatHistory: openChatHistoryId
|
property alias openChatHistory: openChatHistoryId
|
||||||
property alias pinButton: pinButtonId
|
property alias pinButton: pinButtonId
|
||||||
property alias relocateButton: relocateButtonId
|
property alias relocateButton: relocateButtonId
|
||||||
property alias contextButton: contextButtonId
|
|
||||||
property alias toolsButton: toolsButtonId
|
|
||||||
property alias thinkingMode: thinkingModeId
|
|
||||||
property alias settingsButton: settingsButtonId
|
property alias settingsButton: settingsButtonId
|
||||||
property alias configSelector: configSelectorId
|
property alias agentSelector: agentSelectorId
|
||||||
property alias roleSelector: roleSelector
|
|
||||||
property alias relocateTooltip: relocateTooltipId
|
property alias relocateTooltip: relocateTooltipId
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
@@ -134,7 +130,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoAComboBox {
|
||||||
id: configSelectorId
|
id: agentSelectorId
|
||||||
|
|
||||||
implicitHeight: 25
|
implicitHeight: 25
|
||||||
|
|
||||||
@@ -142,87 +138,17 @@ Rectangle {
|
|||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
QoAToolTip {
|
QoAToolTip {
|
||||||
visible: configSelectorId.hovered
|
visible: agentSelectorId.hovered
|
||||||
delay: 250
|
delay: 250
|
||||||
text: qsTr("Switch saved AI configuration")
|
text: qsTr("Select chat agent (provider and model come from the agent)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
|
||||||
id: roleSelector
|
|
||||||
|
|
||||||
implicitHeight: 25
|
|
||||||
|
|
||||||
model: []
|
|
||||||
currentIndex: 0
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: roleSelector.hovered
|
|
||||||
delay: 250
|
|
||||||
text: qsTr("Switch agent role (different system prompts)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: toolsButtonId
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
checkable: true
|
|
||||||
opacity: enabled ? 1.0 : 0.2
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
|
|
||||||
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: toolsButtonId.hovered
|
|
||||||
delay: 250
|
|
||||||
text: {
|
|
||||||
if (!toolsButtonId.enabled) {
|
|
||||||
return qsTr("Tools are disabled in General Settings")
|
|
||||||
}
|
|
||||||
return toolsButtonId.checked
|
|
||||||
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
|
||||||
: qsTr("Tools disabled: Simple conversation without tool access")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: thinkingModeId
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
checkable: true
|
|
||||||
opacity: enabled ? 1.0 : 0.2
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
|
|
||||||
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: thinkingModeId.hovered
|
|
||||||
delay: 250
|
|
||||||
text: thinkingModeId.enabled
|
|
||||||
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
|
|
||||||
: qsTr("Thinking Mode disabled"))
|
|
||||||
: qsTr("Thinking Mode is not available for this provider")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: settingsButtonId
|
id: settingsButtonId
|
||||||
|
|
||||||
@@ -332,23 +258,6 @@ Rectangle {
|
|||||||
|
|
||||||
QoASeparator {}
|
QoASeparator {}
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: contextButtonId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: contextButtonId.hovered
|
|
||||||
delay: 250
|
|
||||||
text: qsTr("View chat context (system prompt, role, rules)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Badge {
|
Badge {
|
||||||
id: tokensBadgeId
|
id: tokensBadgeId
|
||||||
|
|
||||||
|
|||||||
@@ -209,24 +209,4 @@ QString CodeHandler::detectLanguageFromExtension(const QString &extension)
|
|||||||
return extensionToLanguage.value(extension.toLower(), "");
|
return extensionToLanguage.value(extension.toLower(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
|
|
||||||
{
|
|
||||||
static const QRegularExpression
|
|
||||||
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
|
||||||
return regex;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
|
|
||||||
{
|
|
||||||
static const QRegularExpression
|
|
||||||
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
|
|
||||||
return regex;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
|
|
||||||
{
|
|
||||||
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
|
|
||||||
return regex;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -32,10 +32,6 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static QString getCommentPrefix(const QString &language);
|
static QString getCommentPrefix(const QString &language);
|
||||||
|
|
||||||
static const QRegularExpression &getFullCodeBlockRegex();
|
|
||||||
static const QRegularExpression &getPartialStartBlockRegex();
|
|
||||||
static const QRegularExpression &getPartialEndBlockRegex();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
|
||||||
|
|
||||||
#include <settings/ButtonAspect.hpp>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
#include "QodeAssisttr.h"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
ConfigurationManager &ConfigurationManager::instance()
|
|
||||||
{
|
|
||||||
static ConfigurationManager instance;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::init()
|
|
||||||
{
|
|
||||||
setupConnections();
|
|
||||||
updateAllTemplateDescriptions();
|
|
||||||
checkAllTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (!templ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
|
|
||||||
m_generalSettings.qrTemplateDescription.setValue(templ->description());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateAllTemplateDescriptions()
|
|
||||||
{
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (templ->name() == templateAspect.value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplate.setValue(templ->name());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplate.setValue(templ->name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkAllTemplate()
|
|
||||||
{
|
|
||||||
checkTemplate(m_generalSettings.ccTemplate);
|
|
||||||
checkTemplate(m_generalSettings.caTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_generalSettings(Settings::generalSettings())
|
|
||||||
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
|
|
||||||
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
|
|
||||||
{}
|
|
||||||
|
|
||||||
void ConfigurationManager::setupConnections()
|
|
||||||
{
|
|
||||||
using Config = ConfigurationManager;
|
|
||||||
using Button = ButtonAspect;
|
|
||||||
|
|
||||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(
|
|
||||||
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
|
|
||||||
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectProvider()
|
|
||||||
{
|
|
||||||
const auto providersList = m_providersManager.providersNames();
|
|
||||||
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
|
|
||||||
? m_generalSettings.ccProvider
|
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
|
||||||
? m_generalSettings.ccPreset1Provider
|
|
||||||
: settingsButton == &m_generalSettings.qrSelectProvider
|
|
||||||
? m_generalSettings.qrProvider
|
|
||||||
: m_generalSettings.caProvider;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectModel()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
|
|
||||||
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
|
||||||
|
|
||||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
|
||||||
: m_generalSettings.caUrl.volatileValue();
|
|
||||||
|
|
||||||
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrModel
|
|
||||||
: m_generalSettings.caModel);
|
|
||||||
|
|
||||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
|
||||||
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
|
|
||||||
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
provider->getInstalledModels(providerUrl)
|
|
||||||
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
|
||||||
if (modelList.isEmpty()) {
|
|
||||||
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectTemplate()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
|
||||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
|
||||||
|
|
||||||
const auto templateList = isCodeCompletion || isPreset1
|
|
||||||
? m_templateManger.getFimTemplatesForProvider(providerID)
|
|
||||||
: m_templateManger.getChatTemplatesForProvider(providerID);
|
|
||||||
|
|
||||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Template
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrTemplate
|
|
||||||
: m_generalSettings.caTemplate;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectUrl()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QStringList urls;
|
|
||||||
for (const auto &name : m_providersManager.providersNames()) {
|
|
||||||
const auto url = m_providersManager.getProviderByName(name)->url();
|
|
||||||
if (!urls.contains(url))
|
|
||||||
urls.append(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
|
||||||
? m_generalSettings.ccPreset1Url
|
|
||||||
: settingsButton == &m_generalSettings.qrSetUrl
|
|
||||||
? m_generalSettings.qrUrl
|
|
||||||
: m_generalSettings.caUrl;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
|
||||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include "pluginllmcore/PromptTemplateManager.hpp"
|
|
||||||
#include "pluginllmcore/ProvidersManager.hpp"
|
|
||||||
#include "settings/GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
class ConfigurationManager : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
static ConfigurationManager &instance();
|
|
||||||
|
|
||||||
void init();
|
|
||||||
|
|
||||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
|
||||||
void updateAllTemplateDescriptions();
|
|
||||||
void checkTemplate(const Utils::StringAspect &templateAspect);
|
|
||||||
void checkAllTemplate();
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void selectProvider();
|
|
||||||
void selectModel();
|
|
||||||
void selectTemplate();
|
|
||||||
void selectUrl();
|
|
||||||
|
|
||||||
private:
|
|
||||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
|
||||||
~ConfigurationManager() = default;
|
|
||||||
ConfigurationManager(const ConfigurationManager &) = delete;
|
|
||||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
|
||||||
|
|
||||||
Settings::GeneralSettings &m_generalSettings;
|
|
||||||
PluginLLMCore::ProvidersManager &m_providersManager;
|
|
||||||
PluginLLMCore::PromptTemplateManager &m_templateManger;
|
|
||||||
|
|
||||||
void setupConnections();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
@@ -5,9 +5,29 @@
|
|||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QNetworkAccessManager>
|
|
||||||
#include <QNetworkReply>
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
|
#include "sources/common/ContextData.hpp"
|
||||||
|
#include <Agent.hpp>
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <AgentRouter.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include "context/DocumentContextReader.hpp"
|
#include "context/DocumentContextReader.hpp"
|
||||||
@@ -15,30 +35,29 @@
|
|||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include <pluginllmcore/RulesLoader.hpp>
|
#include "sources/settings/PipelinesConfig.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
LLMClientInterface::LLMClientInterface(
|
LLMClientInterface::LLMClientInterface(
|
||||||
const Settings::GeneralSettings &generalSettings,
|
const Settings::GeneralSettings &generalSettings,
|
||||||
const Settings::CodeCompletionSettings &completeSettings,
|
const Settings::CodeCompletionSettings &completeSettings,
|
||||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
AgentFactory &agentFactory,
|
||||||
PluginLLMCore::IPromptProvider *promptProvider,
|
SessionManager &sessionManager,
|
||||||
Context::IDocumentReader &documentReader,
|
Context::IDocumentReader &documentReader,
|
||||||
IRequestPerformanceLogger &performanceLogger)
|
IRequestPerformanceLogger &performanceLogger)
|
||||||
: m_generalSettings(generalSettings)
|
: m_generalSettings(generalSettings)
|
||||||
, m_completeSettings(completeSettings)
|
, m_completeSettings(completeSettings)
|
||||||
, m_providerRegistry(providerRegistry)
|
, m_agentFactory(agentFactory)
|
||||||
, m_promptProvider(promptProvider)
|
, m_sessionManager(sessionManager)
|
||||||
, m_documentReader(documentReader)
|
, m_documentReader(documentReader)
|
||||||
, m_performanceLogger(performanceLogger)
|
, m_performanceLogger(performanceLogger)
|
||||||
, m_contextManager(new Context::ContextManager(this))
|
, m_contextManager(new Context::ContextManager(this))
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
|
|
||||||
LLMClientInterface::~LLMClientInterface()
|
LLMClientInterface::~LLMClientInterface()
|
||||||
{
|
{
|
||||||
handleCancelRequest();
|
cancelAllRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
|
||||||
@@ -51,48 +70,34 @@ void LLMClientInterface::startImpl()
|
|||||||
emit started();
|
emit started();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
void LLMClientInterface::onCompletionFinished(const QString &requestId)
|
||||||
{
|
{
|
||||||
auto it = m_activeRequests.find(requestId);
|
auto it = m_activeRequests.find(requestId);
|
||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
QString fullText;
|
||||||
sendCompletionToClient(fullText, ctx.originalRequest, true);
|
if (Session *session = it.value().session) {
|
||||||
|
if (auto *history = session->history(); history && !history->isEmpty())
|
||||||
|
fullText = history->messages().back().text();
|
||||||
|
}
|
||||||
|
const QJsonObject originalRequest = it.value().originalRequest;
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
sendCompletionToClient(fullText, originalRequest, true);
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
finishRequest(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleRequestFinalized(
|
void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error)
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId) || !info.usage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
|
||||||
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
{
|
||||||
auto it = m_activeRequests.find(requestId);
|
auto it = m_activeRequests.find(requestId);
|
||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
||||||
|
|
||||||
// Send LSP error response to client
|
|
||||||
QJsonObject response;
|
QJsonObject response;
|
||||||
response["jsonrpc"] = "2.0";
|
response["jsonrpc"] = "2.0";
|
||||||
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
|
response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"];
|
||||||
|
|
||||||
QJsonObject errorObject;
|
QJsonObject errorObject;
|
||||||
errorObject["code"] = -32603; // Internal error code
|
errorObject["code"] = -32603; // Internal error code
|
||||||
@@ -100,9 +105,21 @@ void LLMClientInterface::handleRequestFailed(const QString &requestId, const QSt
|
|||||||
response["error"] = errorObject;
|
response["error"] = errorObject;
|
||||||
|
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||||
|
finishRequest(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::finishRequest(const QString &requestId)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
m_performanceLogger.endTimeMeasurement(requestId);
|
||||||
|
|
||||||
|
if (session)
|
||||||
|
m_sessionManager.release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::sendData(const QByteArray &data)
|
void LLMClientInterface::sendData(const QByteArray &data)
|
||||||
@@ -125,7 +142,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
} else if (method == "getCompletionsCycling") {
|
} else if (method == "getCompletionsCycling") {
|
||||||
handleCompletion(request);
|
handleCompletion(request);
|
||||||
} else if (method == "$/cancelRequest") {
|
} else if (method == "$/cancelRequest") {
|
||||||
handleCancelRequest();
|
handleCancelRequest(request);
|
||||||
} else if (method == "exit") {
|
} else if (method == "exit") {
|
||||||
// TODO make exit handler
|
// TODO make exit handler
|
||||||
} else {
|
} else {
|
||||||
@@ -133,28 +150,50 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleCancelRequest()
|
void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
|
||||||
{
|
{
|
||||||
QSet<PluginLLMCore::Provider *> providers;
|
const QJsonValue lspId = request["params"].toObject()["id"];
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.value().provider) {
|
QString matchedKey;
|
||||||
providers.insert(it.value().provider);
|
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||||
|
if (it.value().originalRequest["id"] == lspId) {
|
||||||
|
matchedKey = it.key();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (matchedKey.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("No active completion request to cancel for LSP id %1")
|
||||||
|
.arg(lspId.toVariant().toString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
finishRequest(matchedKey);
|
||||||
disconnect(provider->client(), nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
QJsonObject response;
|
||||||
const RequestContext &ctx = it.value();
|
response["jsonrpc"] = "2.0";
|
||||||
if (ctx.provider) {
|
response[LanguageServerProtocol::idKey] = lspId;
|
||||||
ctx.provider->cancelRequest(it.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
QJsonObject errorObject;
|
||||||
|
errorObject["code"] = -32800;
|
||||||
|
errorObject["message"] = QStringLiteral("Request cancelled");
|
||||||
|
response["error"] = errorObject;
|
||||||
|
|
||||||
|
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Cancelled completion request %1").arg(matchedKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::cancelAllRequests()
|
||||||
|
{
|
||||||
|
const auto requests = m_activeRequests;
|
||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
|
|
||||||
|
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||||
|
m_performanceLogger.endTimeMeasurement(it.key());
|
||||||
|
if (Session *session = it.value().session)
|
||||||
|
m_sessionManager.release(session);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,21 +232,6 @@ void LLMClientInterface::handleShutdown(const QJsonObject &request)
|
|||||||
|
|
||||||
void LLMClientInterface::handleTextDocumentDidOpen(const QJsonObject &request) {}
|
void LLMClientInterface::handleTextDocumentDidOpen(const QJsonObject &request) {}
|
||||||
|
|
||||||
void LLMClientInterface::handleInitialized(const QJsonObject &request)
|
|
||||||
{
|
|
||||||
QJsonObject response;
|
|
||||||
response["jsonrpc"] = "2.0";
|
|
||||||
response["method"] = "initialized";
|
|
||||||
response["params"] = QJsonObject();
|
|
||||||
|
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::handleExit(const QJsonObject &request)
|
|
||||||
{
|
|
||||||
emit finished();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
|
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
|
||||||
{
|
{
|
||||||
QJsonObject response;
|
QJsonObject response;
|
||||||
@@ -220,10 +244,6 @@ void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QSt
|
|||||||
response["error"] = errorObject;
|
response["error"] = errorObject;
|
||||||
|
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||||
|
|
||||||
// End performance measurement if it was started
|
|
||||||
QString requestId = request["id"].toString();
|
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||||
@@ -237,133 +257,103 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto updatedContext = prepareContext(request, documentInfo);
|
const QString agentName = pickCompletionAgent(filePath);
|
||||||
|
if (agentName.isEmpty()) {
|
||||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
QString error = QString("No code completion agent matches: %1").arg(filePath);
|
||||||
|
|
||||||
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
|
||||||
: m_generalSettings.ccPreset1Provider();
|
|
||||||
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
|
|
||||||
: m_generalSettings.ccPreset1Model();
|
|
||||||
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
|
|
||||||
: m_generalSettings.ccPreset1Url();
|
|
||||||
|
|
||||||
const auto provider = m_providerRegistry.getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
sendErrorResponse(request, error);
|
sendErrorResponse(request, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
QString sessionError;
|
||||||
: m_generalSettings.ccPreset1Template();
|
Session *session = m_sessionManager.acquire(agentName, &sessionError);
|
||||||
|
if (!session) {
|
||||||
|
LOG_MESSAGE(sessionError);
|
||||||
|
sendErrorResponse(request, sessionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
Templates::ContextRenderer::Bindings bindings;
|
||||||
|
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||||
|
Utils::FilePath::fromString(filePath)))
|
||||||
|
bindings.projectDir = project->projectDirectory().toFSPathString();
|
||||||
|
bindings.configDir = AgentFactory::userConfigDir();
|
||||||
|
bindings.language = CodeHandler::detectLanguageFromExtension(QFileInfo(filePath).suffix());
|
||||||
|
session->setContextBindings(bindings);
|
||||||
|
|
||||||
if (!promptTemplate) {
|
Templates::ContextData context = prepareContext(request, documentInfo);
|
||||||
QString error = QString("No template found with name: %1").arg(templateName);
|
|
||||||
|
QString editorContext;
|
||||||
|
if (context.fileContext.has_value())
|
||||||
|
editorContext.append(context.fileContext.value());
|
||||||
|
|
||||||
|
if (m_completeSettings.useOpenFilesContext())
|
||||||
|
editorContext.append(m_contextManager->openedFilesContext({filePath}));
|
||||||
|
|
||||||
|
if (!editorContext.isEmpty())
|
||||||
|
session->systemPrompt()->setLayer(QStringLiteral("completion.context"), editorContext);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
session,
|
||||||
|
&Session::finished,
|
||||||
|
this,
|
||||||
|
[this, session](const LLMQore::RequestID &, const QString &) {
|
||||||
|
onCompletionFinished(requestIdForSession(session));
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
session,
|
||||||
|
&Session::failed,
|
||||||
|
this,
|
||||||
|
[this, session](const LLMQore::RequestID &, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onCompletionFailed(requestIdForSession(session), error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (auto *client = session->client())
|
||||||
|
client->setTransferTimeout(static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
|
blocks.push_back(
|
||||||
|
std::make_unique<CompletionContent>(
|
||||||
|
context.prefix.value_or(QString()), context.suffix.value_or(QString())));
|
||||||
|
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
QString error = QString("Failed to start completion request for agent '%1': %2")
|
||||||
|
.arg(agentName, session->lastError().message);
|
||||||
|
m_sessionManager.removeSession(session);
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
sendErrorResponse(request, error);
|
sendErrorResponse(request, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject payload{{"model", modelName}, {"stream", true}};
|
m_activeRequests[requestId] = {request, session};
|
||||||
|
|
||||||
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
|
|
||||||
if (!stopWords.isEmpty())
|
|
||||||
payload["stop"] = stopWords;
|
|
||||||
|
|
||||||
QString systemPrompt;
|
|
||||||
if (m_completeSettings.useSystemPrompt())
|
|
||||||
systemPrompt.append(
|
|
||||||
m_completeSettings.useUserMessageTemplateForCC()
|
|
||||||
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
|
|
||||||
? m_completeSettings.systemPromptForNonFimModels()
|
|
||||||
: m_completeSettings.systemPrompt());
|
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (project) {
|
|
||||||
QString projectRules
|
|
||||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
|
||||||
LOG_MESSAGE("Loaded project rules for completion");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedContext.fileContext.has_value())
|
|
||||||
systemPrompt.append(updatedContext.fileContext.value());
|
|
||||||
|
|
||||||
if (m_completeSettings.useOpenFilesContext()) {
|
|
||||||
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
|
|
||||||
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
|
||||||
if (!updatedContext.filesMetadata) {
|
|
||||||
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
|
|
||||||
}
|
|
||||||
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedContext.systemPrompt = systemPrompt;
|
|
||||||
|
|
||||||
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
|
|
||||||
QString userMessage;
|
|
||||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
|
||||||
userMessage = m_completeSettings.processMessageToFIM(
|
|
||||||
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
|
|
||||||
} else {
|
|
||||||
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO refactor add message
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
|
||||||
messages.append({"user", userMessage});
|
|
||||||
updatedContext.history = messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
provider->prepareRequest(
|
|
||||||
payload,
|
|
||||||
promptTemplate,
|
|
||||||
updatedContext,
|
|
||||||
PluginLLMCore::RequestType::CodeCompletion,
|
|
||||||
false,
|
|
||||||
false);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleRequestFinalized,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
|
||||||
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
|
||||||
|
|
||||||
auto requestId
|
|
||||||
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
m_performanceLogger.startTimeMeasurement(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
|
||||||
|
{
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::loadCached().rosters.codeCompletion;
|
||||||
|
if (roster.isEmpty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
AgentRouter::Context ctx;
|
||||||
|
ctx.filePath = filePath;
|
||||||
|
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
|
||||||
|
Utils::FilePath::fromString(filePath)))
|
||||||
|
ctx.projectName = project->displayName();
|
||||||
|
|
||||||
|
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString LLMClientInterface::requestIdForSession(Session *session) const
|
||||||
|
{
|
||||||
|
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||||
|
if (it.value().session == session)
|
||||||
|
return it.key();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Templates::ContextData LLMClientInterface::prepareContext(
|
||||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||||
{
|
{
|
||||||
QJsonObject params = request["params"].toObject();
|
QJsonObject params = request["params"].toObject();
|
||||||
@@ -377,14 +367,6 @@ PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
|||||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString LLMClientInterface::resolveEndpoint(
|
|
||||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
|
|
||||||
{
|
|
||||||
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
|
||||||
: m_generalSettings.ccCustomEndpoint();
|
|
||||||
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||||
{
|
{
|
||||||
return m_contextManager;
|
return m_contextManager;
|
||||||
@@ -393,15 +375,6 @@ Context::ContextManager *LLMClientInterface::contextManager() const
|
|||||||
void LLMClientInterface::sendCompletionToClient(
|
void LLMClientInterface::sendCompletionToClient(
|
||||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||||
{
|
{
|
||||||
auto filePath = Context::extractFilePathFromRequest(request);
|
|
||||||
auto documentInfo = m_documentReader.readDocument(filePath);
|
|
||||||
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
|
||||||
|
|
||||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
|
||||||
: m_generalSettings.ccPreset1Template();
|
|
||||||
|
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
|
||||||
|
|
||||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||||
|
|
||||||
QJsonObject response;
|
QJsonObject response;
|
||||||
@@ -420,13 +393,12 @@ void LLMClientInterface::sendCompletionToClient(
|
|||||||
if (outputHandler == "Raw text") {
|
if (outputHandler == "Raw text") {
|
||||||
processedCompletion = completion;
|
processedCompletion = completion;
|
||||||
} else if (outputHandler == "Force processing") {
|
} else if (outputHandler == "Force processing") {
|
||||||
processedCompletion = CodeHandler::processText(completion,
|
processedCompletion
|
||||||
Context::extractFilePathFromRequest(request));
|
= CodeHandler::processText(completion, Context::extractFilePathFromRequest(request));
|
||||||
} else { // "Auto"
|
} else { // "Auto"
|
||||||
processedCompletion = CodeHandler::hasCodeBlocks(completion)
|
processedCompletion
|
||||||
? CodeHandler::processText(completion,
|
= CodeHandler::hasCodeBlocks(completion)
|
||||||
Context::extractFilePathFromRequest(
|
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
|
||||||
request))
|
|
||||||
: completion;
|
: completion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,8 +431,6 @@ void LLMClientInterface::sendCompletionToClient(
|
|||||||
QString("Full response: \n%1")
|
QString("Full response: \n%1")
|
||||||
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
|
||||||
|
|
||||||
QString requestId = request["id"].toString();
|
|
||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
|
||||||
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,25 @@
|
|||||||
#include <languageclient/languageclientinterface.h>
|
#include <languageclient/languageclientinterface.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
#include <context/IDocumentReader.hpp>
|
#include <context/IDocumentReader.hpp>
|
||||||
#include <context/ProgrammingLanguage.hpp>
|
#include <context/ProgrammingLanguage.hpp>
|
||||||
#include <pluginllmcore/ContextData.hpp>
|
|
||||||
#include <pluginllmcore/IPromptProvider.hpp>
|
|
||||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
|
||||||
#include <logger/IRequestPerformanceLogger.hpp>
|
#include <logger/IRequestPerformanceLogger.hpp>
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
|
|
||||||
class QNetworkReply;
|
|
||||||
class QNetworkAccessManager;
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class AgentFactory;
|
||||||
|
class Session;
|
||||||
|
class SessionManager;
|
||||||
|
|
||||||
|
namespace Templates {
|
||||||
|
struct ContextData;
|
||||||
|
}
|
||||||
|
|
||||||
class LLMClientInterface : public LanguageClient::BaseClientInterface
|
class LLMClientInterface : public LanguageClient::BaseClientInterface
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -31,8 +35,8 @@ public:
|
|||||||
LLMClientInterface(
|
LLMClientInterface(
|
||||||
const Settings::GeneralSettings &generalSettings,
|
const Settings::GeneralSettings &generalSettings,
|
||||||
const Settings::CodeCompletionSettings &completeSettings,
|
const Settings::CodeCompletionSettings &completeSettings,
|
||||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
AgentFactory &agentFactory,
|
||||||
PluginLLMCore::IPromptProvider *promptProvider,
|
SessionManager &sessionManager,
|
||||||
Context::IDocumentReader &documentReader,
|
Context::IDocumentReader &documentReader,
|
||||||
IRequestPerformanceLogger &performanceLogger);
|
IRequestPerformanceLogger &performanceLogger);
|
||||||
~LLMClientInterface() override;
|
~LLMClientInterface() override;
|
||||||
@@ -44,7 +48,6 @@ public:
|
|||||||
|
|
||||||
void handleCompletion(const QJsonObject &request);
|
void handleCompletion(const QJsonObject &request);
|
||||||
|
|
||||||
// exposed for tests
|
|
||||||
void sendData(const QByteArray &data) override;
|
void sendData(const QByteArray &data) override;
|
||||||
|
|
||||||
Context::ContextManager *contextManager() const;
|
Context::ContextManager *contextManager() const;
|
||||||
@@ -52,40 +55,36 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
void startImpl() override;
|
void startImpl() override;
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleInitialize(const QJsonObject &request);
|
void handleInitialize(const QJsonObject &request);
|
||||||
void handleShutdown(const QJsonObject &request);
|
void handleShutdown(const QJsonObject &request);
|
||||||
void handleTextDocumentDidOpen(const QJsonObject &request);
|
void handleTextDocumentDidOpen(const QJsonObject &request);
|
||||||
void handleInitialized(const QJsonObject &request);
|
void handleCancelRequest(const QJsonObject &request);
|
||||||
void handleExit(const QJsonObject &request);
|
void cancelAllRequests();
|
||||||
void handleCancelRequest();
|
|
||||||
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
||||||
|
|
||||||
|
void onCompletionFinished(const QString &requestId);
|
||||||
|
void onCompletionFailed(const QString &requestId, const QString &error);
|
||||||
|
void finishRequest(const QString &requestId);
|
||||||
|
QString requestIdForSession(Session *session) const;
|
||||||
|
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
PluginLLMCore::Provider *provider;
|
QPointer<Session> session;
|
||||||
};
|
};
|
||||||
|
|
||||||
PluginLLMCore::ContextData prepareContext(
|
Templates::ContextData prepareContext(
|
||||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||||
|
|
||||||
QString resolveEndpoint(
|
QString pickCompletionAgent(const QString &filePath) const;
|
||||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
|
|
||||||
|
|
||||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
|
||||||
const Settings::GeneralSettings &m_generalSettings;
|
const Settings::GeneralSettings &m_generalSettings;
|
||||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||||
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
AgentFactory &m_agentFactory;
|
||||||
|
SessionManager &m_sessionManager;
|
||||||
Context::IDocumentReader &m_documentReader;
|
Context::IDocumentReader &m_documentReader;
|
||||||
IRequestPerformanceLogger &m_performanceLogger;
|
IRequestPerformanceLogger &m_performanceLogger;
|
||||||
QElapsedTimer m_completionTimer;
|
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ namespace QodeAssist {
|
|||||||
class Completion : public LanguageServerProtocol::JsonObject
|
class Completion : public LanguageServerProtocol::JsonObject
|
||||||
{
|
{
|
||||||
static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"};
|
static constexpr LanguageServerProtocol::Key displayTextKey{"displayText"};
|
||||||
static constexpr LanguageServerProtocol::Key uuidKey{"uuid"};
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
using JsonObject::JsonObject;
|
using JsonObject::JsonObject;
|
||||||
@@ -55,7 +54,6 @@ public:
|
|||||||
}
|
}
|
||||||
QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); }
|
QString text() const { return typedValue<QString>(LanguageServerProtocol::textKey); }
|
||||||
void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); }
|
void setText(const QString &text) { insert(LanguageServerProtocol::textKey, text); }
|
||||||
QString uuid() const { return typedValue<QString>(uuidKey); }
|
|
||||||
|
|
||||||
bool isValid() const override
|
bool isValid() const override
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -159,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
|
||||||
|
{
|
||||||
|
m_agentFactory = agentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
@@ -262,10 +272,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (m_llmClient->contextManager()->shouldIgnore(
|
||||||
if (m_llmClient->contextManager()
|
editor->textDocument()->filePath().toUrlishString())) {
|
||||||
->ignoreManager()
|
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||||
return;
|
return;
|
||||||
@@ -297,6 +305,12 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
QTC_ASSERT(editor, return);
|
QTC_ASSERT(editor, return);
|
||||||
handleCompletions(response, editor);
|
handleCompletions(response, editor);
|
||||||
});
|
});
|
||||||
|
connect(
|
||||||
|
editor,
|
||||||
|
&TextEditorWidget::destroyed,
|
||||||
|
this,
|
||||||
|
&QodeAssistClient::onEditorDestroyed,
|
||||||
|
Qt::UniqueConnection);
|
||||||
m_runningRequests[editor] = request;
|
m_runningRequests[editor] = request;
|
||||||
sendMessage(request);
|
sendMessage(request);
|
||||||
}
|
}
|
||||||
@@ -309,9 +323,8 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
if (m_llmClient->contextManager()->shouldIgnore(
|
||||||
->ignoreManager()
|
editor->textDocument()->filePath().toUrlishString())) {
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||||
return;
|
return;
|
||||||
@@ -319,6 +332,8 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
|
|
||||||
if (!m_refactorHandler) {
|
if (!m_refactorHandler) {
|
||||||
m_refactorHandler = new QuickRefactorHandler(this);
|
m_refactorHandler = new QuickRefactorHandler(this);
|
||||||
|
m_refactorHandler->setSessionManager(m_sessionManager);
|
||||||
|
m_refactorHandler->setAgentFactory(m_agentFactory);
|
||||||
connect(
|
connect(
|
||||||
m_refactorHandler,
|
m_refactorHandler,
|
||||||
&QuickRefactorHandler::refactoringCompleted,
|
&QuickRefactorHandler::refactoringCompleted,
|
||||||
@@ -357,10 +372,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
|||||||
return;
|
return;
|
||||||
requestCompletions(editor);
|
requestCompletions(editor);
|
||||||
});
|
});
|
||||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
connect(
|
||||||
delete m_scheduledRequests.take(editor);
|
editor,
|
||||||
cancelRunningRequest(editor);
|
&TextEditorWidget::destroyed,
|
||||||
});
|
this,
|
||||||
|
&QodeAssistClient::onEditorDestroyed,
|
||||||
|
Qt::UniqueConnection);
|
||||||
it = m_scheduledRequests.insert(editor, timer);
|
it = m_scheduledRequests.insert(editor, timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,9 +393,12 @@ void QodeAssistClient::handleCompletions(
|
|||||||
editor->abortAssist();
|
editor->abortAssist();
|
||||||
|
|
||||||
if (response.error()) {
|
if (response.error()) {
|
||||||
|
m_runningRequests.remove(editor);
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
|
if (response.error()->code() != -32800) {
|
||||||
m_errorHandler
|
m_errorHandler
|
||||||
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
|
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,6 +499,13 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
|||||||
m_runningRequests.erase(it);
|
m_runningRequests.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::onEditorDestroyed(QObject *editorObject)
|
||||||
|
{
|
||||||
|
auto *editor = static_cast<TextEditor::TextEditorWidget *>(editorObject);
|
||||||
|
delete m_scheduledRequests.take(editor);
|
||||||
|
cancelRunningRequest(editor);
|
||||||
|
}
|
||||||
|
|
||||||
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
|
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
|
||||||
{
|
{
|
||||||
if (!project)
|
if (!project)
|
||||||
@@ -520,6 +547,11 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
{
|
{
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
|
|
||||||
|
if (result.cancelled) {
|
||||||
|
LOG_MESSAGE("Refactoring request was cancelled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
QString errorMessage = result.errorMessage.isEmpty()
|
QString errorMessage = result.errorMessage.isEmpty()
|
||||||
? tr("Quick refactor failed")
|
? tr("Quick refactor failed")
|
||||||
@@ -538,6 +570,15 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.documentRevision >= 0
|
||||||
|
&& result.editor->document()->revision() != result.documentRevision) {
|
||||||
|
m_errorHandler.showError(
|
||||||
|
result.editor,
|
||||||
|
tr("Quick refactor discarded: the document changed while the request was running"));
|
||||||
|
LOG_MESSAGE("Refactoring result discarded: document revision changed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int displayMode = Settings::quickRefactorSettings().displayMode();
|
int displayMode = Settings::quickRefactorSettings().displayMode();
|
||||||
|
|
||||||
if (displayMode == 0) {
|
if (displayMode == 0) {
|
||||||
@@ -634,7 +675,14 @@ void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
|
|||||||
displayRefactored = result.newText;
|
displayRefactored = result.newText;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
|
const int revisionAtDisplay = editorWidget->document()->revision();
|
||||||
|
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result, revisionAtDisplay](
|
||||||
|
const QString &editedText) {
|
||||||
|
if (editorWidget->document()->revision() != revisionAtDisplay) {
|
||||||
|
m_errorHandler.showError(
|
||||||
|
editorWidget, tr("Quick refactor discarded: the document changed before applying"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
applyRefactoringEdit(editorWidget, result.insertRange, editedText);
|
applyRefactoringEdit(editorWidget, result.insertRange, editedText);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,21 +6,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
#include "LSPCompletion.hpp"
|
#include "LSPCompletion.hpp"
|
||||||
#include "QuickRefactorHandler.hpp"
|
#include "QuickRefactorHandler.hpp"
|
||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
#include "RefactorSuggestionHoverHandler.hpp"
|
||||||
#include "widgets/CompletionProgressHandler.hpp"
|
|
||||||
#include "widgets/CompletionErrorHandler.hpp"
|
#include "widgets/CompletionErrorHandler.hpp"
|
||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
#include "widgets/CompletionProgressHandler.hpp"
|
||||||
#include "widgets/RefactorWidgetHandler.hpp"
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
#include <pluginllmcore/IPromptProvider.hpp>
|
|
||||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class SessionManager;
|
||||||
|
class AgentFactory;
|
||||||
|
|
||||||
class QodeAssistClient : public LanguageClient::Client
|
class QodeAssistClient : public LanguageClient::Client
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -28,6 +29,9 @@ public:
|
|||||||
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||||
~QodeAssistClient() override;
|
~QodeAssistClient() override;
|
||||||
|
|
||||||
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setAgentFactory(AgentFactory *agentFactory);
|
||||||
|
|
||||||
void openDocument(TextEditor::TextDocument *document) override;
|
void openDocument(TextEditor::TextDocument *document) override;
|
||||||
bool canOpenProject(ProjectExplorer::Project *project) override;
|
bool canOpenProject(ProjectExplorer::Project *project) override;
|
||||||
|
|
||||||
@@ -43,6 +47,7 @@ private:
|
|||||||
void handleCompletions(
|
void handleCompletions(
|
||||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
|
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
|
||||||
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
|
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
|
||||||
|
void onEditorDestroyed(QObject *editorObject);
|
||||||
bool isEnabled(ProjectExplorer::Project *project) const;
|
bool isEnabled(ProjectExplorer::Project *project) const;
|
||||||
|
|
||||||
void setupConnections();
|
void setupConnections();
|
||||||
@@ -63,11 +68,12 @@ private:
|
|||||||
int m_recentCharCount;
|
int m_recentCharCount;
|
||||||
CompletionProgressHandler m_progressHandler;
|
CompletionProgressHandler m_progressHandler;
|
||||||
CompletionErrorHandler m_errorHandler;
|
CompletionErrorHandler m_errorHandler;
|
||||||
EditorChatButtonHandler m_chatButtonHandler;
|
|
||||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||||
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||||
LLMClientInterface *m_llmClient;
|
LLMClientInterface *m_llmClient;
|
||||||
|
SessionManager *m_sessionManager{nullptr};
|
||||||
|
AgentFactory *m_agentFactory{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -4,23 +4,39 @@
|
|||||||
|
|
||||||
#include "QuickRefactorHandler.hpp"
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <QJsonArray>
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
#include <QJsonDocument>
|
#include <LLMQore/ToolLoopRunner.hpp>
|
||||||
#include <QUuid>
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
|
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
#include <context/DocumentContextReader.hpp>
|
#include <context/DocumentContextReader.hpp>
|
||||||
#include <pluginllmcore/ResponseCleaner.hpp>
|
|
||||||
#include <context/DocumentReaderQtCreator.hpp>
|
#include <context/DocumentReaderQtCreator.hpp>
|
||||||
|
#include <context/EnvBlockFormatter.hpp>
|
||||||
#include <context/Utils.hpp>
|
#include <context/Utils.hpp>
|
||||||
#include <pluginllmcore/PromptTemplateManager.hpp>
|
|
||||||
#include <pluginllmcore/ProvidersManager.hpp>
|
|
||||||
#include <pluginllmcore/RulesLoader.hpp>
|
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
#include <settings/ChatAssistantSettings.hpp>
|
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
#include <settings/QuickRefactorSettings.hpp>
|
#include <settings/QuickRefactorSettings.hpp>
|
||||||
#include <settings/ToolsSettings.hpp>
|
#include <settings/ToolsSettings.hpp>
|
||||||
|
#include <sources/common/ResponseCleaner.hpp>
|
||||||
|
|
||||||
|
#include "sources/common/ContextData.hpp"
|
||||||
|
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <ContextRenderer.hpp>
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
|
||||||
|
#include "sources/settings/PipelinesConfig.hpp"
|
||||||
|
#include "tools/ToolsRegistration.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
@@ -34,14 +50,25 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
|||||||
|
|
||||||
QuickRefactorHandler::~QuickRefactorHandler() {}
|
QuickRefactorHandler::~QuickRefactorHandler() {}
|
||||||
|
|
||||||
|
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
|
||||||
|
{
|
||||||
|
m_agentFactory = agentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::sendRefactorRequest(
|
void QuickRefactorHandler::sendRefactorRequest(
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
TextEditor::TextEditorWidget *editor, const QString &instructions)
|
||||||
{
|
{
|
||||||
if (m_isRefactoringInProgress) {
|
if (m_isRefactoringInProgress) {
|
||||||
cancelRequest();
|
abortActiveRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentEditor = editor;
|
m_currentEditor = editor;
|
||||||
|
m_currentDocumentRevision = editor->document()->revision();
|
||||||
|
|
||||||
Utils::Text::Range range;
|
Utils::Text::Range range;
|
||||||
if (editor->textCursor().hasSelection()) {
|
if (editor->textCursor().hasSelection()) {
|
||||||
@@ -88,105 +115,117 @@ void QuickRefactorHandler::sendRefactorRequest(
|
|||||||
prepareAndSendRequest(editor, instructions, range);
|
prepareAndSendRequest(editor, instructions, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString QuickRefactorHandler::configuredAgent(AgentFactory *agentFactory)
|
||||||
|
{
|
||||||
|
const QString configured = Settings::PipelinesConfig::loadCached().rosters.quickRefactor;
|
||||||
|
if (configured.isEmpty() || !agentFactory || !agentFactory->configByName(configured))
|
||||||
|
return {};
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QuickRefactorHandler::pickRefactorAgent() const
|
||||||
|
{
|
||||||
|
return configuredAgent(m_agentFactory);
|
||||||
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::prepareAndSendRequest(
|
void QuickRefactorHandler::prepareAndSendRequest(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor,
|
||||||
const QString &instructions,
|
const QString &instructions,
|
||||||
const Utils::Text::Range &range)
|
const Utils::Text::Range &range)
|
||||||
{
|
{
|
||||||
auto &settings = Settings::generalSettings();
|
const auto emitError = [this, editor](const QString &error) {
|
||||||
|
|
||||||
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
|
|
||||||
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
|
|
||||||
|
|
||||||
const auto providerName = settings.qrProvider();
|
|
||||||
auto provider = providerRegistry.getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (!provider) {
|
|
||||||
QString error = QString("No provider found with name: %1").arg(providerName);
|
|
||||||
LOG_MESSAGE(error);
|
LOG_MESSAGE(error);
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.success = false;
|
result.success = false;
|
||||||
result.errorMessage = error;
|
result.errorMessage = error;
|
||||||
result.editor = editor;
|
result.editor = editor;
|
||||||
emit refactoringCompleted(result);
|
emit refactoringCompleted(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!m_sessionManager) {
|
||||||
|
emitError(QStringLiteral("Quick refactor session manager is not available"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto templateName = settings.qrTemplate();
|
const QString agentName = pickRefactorAgent();
|
||||||
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
if (agentName.isEmpty()) {
|
||||||
|
emitError(
|
||||||
if (!promptTemplate) {
|
QStringLiteral("No quick refactor agent configured. Set one in QodeAssist > General."));
|
||||||
QString error = QString("No template found with name: %1").arg(templateName);
|
|
||||||
LOG_MESSAGE(error);
|
|
||||||
RefactorResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.errorMessage = error;
|
|
||||||
result.editor = editor;
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject payload{
|
QString sessionError;
|
||||||
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
Session *session = m_sessionManager->acquire(agentName, &sessionError);
|
||||||
|
if (!session) {
|
||||||
|
emitError(
|
||||||
|
sessionError.isEmpty() ? QStringLiteral("No quick refactor agent selected")
|
||||||
|
: sessionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
|
auto *client = session->client();
|
||||||
|
if (!client) {
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emitError(QStringLiteral("Quick refactor agent has no live client"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool enableTools = Settings::quickRefactorSettings().useTools();
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
Templates::ContextRenderer::Bindings bindings;
|
||||||
provider->prepareRequest(
|
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
||||||
payload,
|
bindings.configDir = AgentFactory::userConfigDir();
|
||||||
promptTemplate,
|
session->setContextBindings(bindings);
|
||||||
context,
|
|
||||||
PluginLLMCore::RequestType::QuickRefactoring,
|
|
||||||
enableTools,
|
|
||||||
enableThinking);
|
|
||||||
|
|
||||||
provider->client()->setMaxToolContinuations(
|
const AgentConfig *agentConfig = m_agentFactory ? m_agentFactory->configByName(agentName)
|
||||||
Settings::toolsSettings().maxToolContinuations());
|
: nullptr;
|
||||||
|
if (agentConfig && agentConfig->enableTools) {
|
||||||
|
m_sessionManager->toolContributors().contribute(client->tools());
|
||||||
|
client->toolLoop()->setMaxRounds(Settings::toolsSettings().maxToolContinuations());
|
||||||
|
}
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
session->systemPrompt()->setLayer(QStringLiteral("refactor"), buildContextLayer(editor, range));
|
||||||
|
|
||||||
|
client->setTransferTimeout(
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
|
|
||||||
m_isRefactoringInProgress = true;
|
m_isRefactoringInProgress = true;
|
||||||
|
|
||||||
|
connect(session, &Session::finished, this, [this](const LLMQore::RequestID &id, const QString &) {
|
||||||
|
onRefactorFinished(id);
|
||||||
|
});
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
session,
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
&Session::failed,
|
||||||
this,
|
this,
|
||||||
&QuickRefactorHandler::handleFullResponse,
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
Qt::UniqueConnection);
|
onRefactorFailed(id, error);
|
||||||
|
});
|
||||||
|
|
||||||
connect(
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
provider->client(),
|
const QString userMessage
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
= instructions.isEmpty()
|
||||||
this,
|
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
|
||||||
&QuickRefactorHandler::handleRequestFinalized,
|
: instructions;
|
||||||
Qt::UniqueConnection);
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
|
||||||
|
|
||||||
connect(
|
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
||||||
provider->client(),
|
if (requestId.isEmpty()) {
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
m_isRefactoringInProgress = false;
|
||||||
this,
|
const QString reason = session->lastError().message;
|
||||||
&QuickRefactorHandler::handleRequestFailed,
|
m_sessionManager->removeSession(session);
|
||||||
Qt::UniqueConnection);
|
emitError(QStringLiteral("Failed to start quick refactor request for agent '%1': %2")
|
||||||
|
.arg(agentName, reason));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
|
||||||
: promptTemplate->endpoint();
|
|
||||||
auto requestId
|
|
||||||
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
|
||||||
m_lastRequestId = requestId;
|
m_lastRequestId = requestId;
|
||||||
QJsonObject request{{"id", requestId}};
|
m_activeSession = session;
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
QString QuickRefactorHandler::buildContextLayer(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
|
||||||
const Utils::Text::Range &range,
|
|
||||||
const QString &instructions)
|
|
||||||
{
|
{
|
||||||
PluginLLMCore::ContextData context;
|
Q_UNUSED(range)
|
||||||
|
|
||||||
auto textDocument = editor->textDocument();
|
auto textDocument = editor->textDocument();
|
||||||
Context::DocumentReaderQtCreator documentReader;
|
Context::DocumentReaderQtCreator documentReader;
|
||||||
@@ -194,7 +233,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
|
|
||||||
if (!documentInfo.document) {
|
if (!documentInfo.document) {
|
||||||
LOG_MESSAGE("Error: Document is not available");
|
LOG_MESSAGE("Error: Document is not available");
|
||||||
return context;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextCursor cursor = editor->textCursor();
|
QTextCursor cursor = editor->textCursor();
|
||||||
@@ -268,43 +307,21 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
taggedContent = contextBefore + "<cursor>" + contextAfter;
|
taggedContent = contextBefore + "<cursor>" + contextAfter;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
QString contextLayer = Context::EnvBlockFormatter::formatFile(
|
||||||
|
{documentInfo.filePath, documentInfo.mimeType});
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
contextLayer += "\n# Code Context with Position Markers\n" + taggedContent;
|
||||||
if (project) {
|
|
||||||
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
|
||||||
project, PluginLLMCore::RulesContext::QuickRefactor);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
contextLayer += "\n\n# What to Generate:";
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
contextLayer
|
||||||
LOG_MESSAGE("Loaded project rules for quick refactor");
|
+= cursor.hasSelection()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
systemPrompt += "\n\nFile information:";
|
|
||||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
|
||||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
|
||||||
|
|
||||||
systemPrompt += "\n\n# Code Context with Position Markers\n" + taggedContent;
|
|
||||||
|
|
||||||
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
|
|
||||||
systemPrompt += cursor.hasSelection()
|
|
||||||
? "\n- Generate ONLY the code that should REPLACE the selected text between "
|
? "\n- Generate ONLY the code that should REPLACE the selected text between "
|
||||||
"<selection_start> and <selection_end> markers"
|
"<selection_start> and <selection_end> markers"
|
||||||
"\n- Your output will completely replace the selected code"
|
"\n- Your output will completely replace the selected code"
|
||||||
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||||
"\n- Your output will be inserted at the cursor location";
|
"\n- Your output will be inserted at the cursor location";
|
||||||
|
|
||||||
systemPrompt += "\n\n## Formatting Rules:"
|
QString indentNote;
|
||||||
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
|
||||||
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
|
||||||
"\n- Do NOT add comments explaining what you changed"
|
|
||||||
"\n- Do NOT repeat existing code, be precise with context"
|
|
||||||
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
|
||||||
"\n- The output must be ready to insert directly into the editor as-is";
|
|
||||||
|
|
||||||
systemPrompt += "\n\n## Indentation and Whitespace:";
|
|
||||||
|
|
||||||
if (cursor.hasSelection()) {
|
if (cursor.hasSelection()) {
|
||||||
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||||
int leadingSpaces = 0;
|
int leadingSpaces = 0;
|
||||||
@@ -314,12 +331,14 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
if (leadingSpaces > 0) {
|
if (leadingSpaces > 0) {
|
||||||
systemPrompt += QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
indentNote
|
||||||
|
= QString(
|
||||||
|
"\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
||||||
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
||||||
"\n- Each line in your output must maintain this base indentation")
|
"\n- Each line in your output must maintain this base indentation")
|
||||||
.arg(leadingSpaces);
|
.arg(leadingSpaces);
|
||||||
}
|
}
|
||||||
systemPrompt += "\n- PRESERVE all indentation from the original code";
|
indentNote += "\n- PRESERVE all indentation from the original code";
|
||||||
} else {
|
} else {
|
||||||
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
||||||
QString lineText = block.text();
|
QString lineText = block.text();
|
||||||
@@ -330,61 +349,37 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
else break;
|
else break;
|
||||||
}
|
}
|
||||||
if (leadingSpaces > 0) {
|
if (leadingSpaces > 0) {
|
||||||
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation"
|
indentNote = QString(
|
||||||
"\n- If generating multiline code, EVERY line must start with at least %1 spaces"
|
"\n- CRITICAL: Current line has %1 spaces of indentation"
|
||||||
"\n- If generating single-line code, it will be inserted inline (no indentation needed)")
|
"\n- If generating multiline code, EVERY line must start with at "
|
||||||
|
"least %1 spaces"
|
||||||
|
"\n- If generating single-line code, it will be inserted inline (no "
|
||||||
|
"indentation needed)")
|
||||||
.arg(leadingSpaces);
|
.arg(leadingSpaces);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!indentNote.isEmpty())
|
||||||
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
contextLayer += "\n\n## Indentation:" + indentNote;
|
||||||
"\n- Maintain consistent indentation for nested blocks"
|
|
||||||
"\n- Do NOT remove or reduce the base indentation level"
|
|
||||||
"\n\n## Code Style:"
|
|
||||||
"\n- Match the coding style of the surrounding code (naming, spacing, braces, etc.)"
|
|
||||||
"\n- Preserve the original code structure when possible"
|
|
||||||
"\n- Only change what is necessary to fulfill the user's request";
|
|
||||||
|
|
||||||
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
|
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
|
||||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
contextLayer += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||||
}
|
}
|
||||||
|
|
||||||
context.systemPrompt = systemPrompt;
|
return contextLayer;
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
|
||||||
messages.append(
|
|
||||||
{"user",
|
|
||||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
|
||||||
: instructions});
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::handleLLMResponse(
|
void QuickRefactorHandler::abortActiveRequest()
|
||||||
const QString &response, const QJsonObject &request, bool isComplete)
|
|
||||||
{
|
{
|
||||||
if (request["id"].toString() != m_lastRequestId) {
|
if (!m_isRefactoringInProgress)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (isComplete) {
|
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
RefactorResult result;
|
Session *session = m_activeSession;
|
||||||
result.newText = cleanedResponse;
|
m_activeSession = nullptr;
|
||||||
result.insertRange = m_currentRange;
|
if (session && m_sessionManager)
|
||||||
result.success = true;
|
m_sessionManager->release(session);
|
||||||
result.editor = m_currentEditor;
|
|
||||||
|
|
||||||
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
|
||||||
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
|
||||||
LOG_MESSAGE(cleanedResponse);
|
|
||||||
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
|
||||||
|
|
||||||
emit refactoringCompleted(result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::cancelRequest()
|
void QuickRefactorHandler::cancelRequest()
|
||||||
@@ -392,60 +387,73 @@ void QuickRefactorHandler::cancelRequest()
|
|||||||
if (!m_isRefactoringInProgress)
|
if (!m_isRefactoringInProgress)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto id = m_lastRequestId;
|
abortActiveRequest();
|
||||||
|
|
||||||
|
RefactorResult result;
|
||||||
|
result.success = false;
|
||||||
|
result.cancelled = true;
|
||||||
|
result.errorMessage = "Refactoring request was cancelled";
|
||||||
|
result.editor = m_currentEditor;
|
||||||
|
emit refactoringCompleted(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
|
||||||
|
{
|
||||||
|
if (requestId != m_lastRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = m_activeSession;
|
||||||
|
m_activeSession = nullptr;
|
||||||
|
|
||||||
|
QString fullText;
|
||||||
|
if (session) {
|
||||||
|
if (auto *history = session->history(); history && !history->isEmpty())
|
||||||
|
fullText = history->messages().back().text();
|
||||||
|
}
|
||||||
|
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
m_lastRequestId.clear();
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
auto it = m_activeRequests.find(id);
|
const QString cleanedResponse = ResponseCleaner::clean(fullText);
|
||||||
if (it != m_activeRequests.end()) {
|
|
||||||
auto provider = it.value().provider;
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
if (provider)
|
|
||||||
provider->cancelRequest(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.success = false;
|
result.newText = cleanedResponse;
|
||||||
result.errorMessage = "Refactoring request was cancelled";
|
result.insertRange = m_currentRange;
|
||||||
|
result.success = true;
|
||||||
|
result.editor = m_currentEditor;
|
||||||
|
result.documentRevision = m_currentDocumentRevision;
|
||||||
|
|
||||||
|
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
|
||||||
|
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
|
||||||
|
LOG_MESSAGE(cleanedResponse);
|
||||||
|
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
||||||
|
|
||||||
emit refactoringCompleted(result);
|
emit refactoringCompleted(result);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
void QuickRefactorHandler::onRefactorFailed(
|
||||||
|
const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||||
{
|
{
|
||||||
if (requestId == m_lastRequestId) {
|
if (requestId != m_lastRequestId)
|
||||||
m_activeRequests.remove(requestId);
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
handleLLMResponse(fullText, request, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (requestId != m_lastRequestId || !info.usage)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
Session *session = m_activeSession;
|
||||||
LOG_MESSAGE(
|
m_activeSession = nullptr;
|
||||||
QString("Quick refactor usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
if (requestId == m_lastRequestId) {
|
|
||||||
m_activeRequests.remove(requestId);
|
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.success = false;
|
result.success = false;
|
||||||
result.errorMessage = error;
|
result.errorMessage = error.message;
|
||||||
result.editor = m_currentEditor;
|
result.editor = m_currentEditor;
|
||||||
emit refactoringCompleted(result);
|
emit refactoringCompleted(result);
|
||||||
}
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -4,27 +4,32 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
#include <utils/textutils.h>
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
#include <ErrorInfo.hpp>
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
#include <context/IDocumentReader.hpp>
|
#include <context/IDocumentReader.hpp>
|
||||||
#include <pluginllmcore/ContextData.hpp>
|
|
||||||
#include <pluginllmcore/Provider.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class SessionManager;
|
||||||
|
class Session;
|
||||||
|
class AgentFactory;
|
||||||
|
|
||||||
struct RefactorResult
|
struct RefactorResult
|
||||||
{
|
{
|
||||||
QString newText;
|
QString newText;
|
||||||
Utils::Text::Range insertRange;
|
Utils::Text::Range insertRange;
|
||||||
bool success;
|
bool success = false;
|
||||||
|
bool cancelled = false;
|
||||||
QString errorMessage;
|
QString errorMessage;
|
||||||
TextEditor::TextEditorWidget *editor{nullptr};
|
QPointer<TextEditor::TextEditorWidget> editor;
|
||||||
|
int documentRevision = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
class QuickRefactorHandler : public QObject
|
class QuickRefactorHandler : public QObject
|
||||||
@@ -35,43 +40,39 @@ public:
|
|||||||
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
explicit QuickRefactorHandler(QObject *parent = nullptr);
|
||||||
~QuickRefactorHandler() override;
|
~QuickRefactorHandler() override;
|
||||||
|
|
||||||
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setAgentFactory(AgentFactory *agentFactory);
|
||||||
|
|
||||||
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
|
||||||
|
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
bool isProcessing() const { return m_isRefactoringInProgress; }
|
bool isProcessing() const { return m_isRefactoringInProgress; }
|
||||||
|
|
||||||
|
static QString configuredAgent(AgentFactory *agentFactory);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
void refactoringCompleted(const QodeAssist::RefactorResult &result);
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
void handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void prepareAndSendRequest(
|
void prepareAndSendRequest(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor,
|
||||||
const QString &instructions,
|
const QString &instructions,
|
||||||
const Utils::Text::Range &range);
|
const Utils::Text::Range &range);
|
||||||
|
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
void onRefactorFinished(const QString &requestId);
|
||||||
PluginLLMCore::ContextData prepareContext(
|
void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||||
TextEditor::TextEditorWidget *editor,
|
QString buildContextLayer(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
|
||||||
const Utils::Text::Range &range,
|
QString pickRefactorAgent() const;
|
||||||
const QString &instructions);
|
void abortActiveRequest();
|
||||||
|
|
||||||
struct RequestContext
|
QPointer<SessionManager> m_sessionManager;
|
||||||
{
|
QPointer<AgentFactory> m_agentFactory;
|
||||||
QJsonObject originalRequest;
|
QPointer<Session> m_activeSession;
|
||||||
PluginLLMCore::Provider *provider;
|
QPointer<TextEditor::TextEditorWidget> m_currentEditor;
|
||||||
};
|
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
|
||||||
TextEditor::TextEditorWidget *m_currentEditor;
|
|
||||||
Utils::Text::Range m_currentRange;
|
Utils::Text::Range m_currentRange;
|
||||||
bool m_isRefactoringInProgress;
|
bool m_isRefactoringInProgress;
|
||||||
QString m_lastRequestId;
|
QString m_lastRequestId;
|
||||||
|
int m_currentDocumentRevision = -1;
|
||||||
Context::ContextManager m_contextManager;
|
Context::ContextManager m_contextManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
149
README.md
149
README.md
@@ -39,9 +39,9 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
|
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
|
||||||
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
|
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
|
||||||
- **File Context** — attach, link, or auto-sync open editor files for richer prompts
|
- **File Context** — attach, link, or auto-sync open editor files for richer prompts
|
||||||
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint
|
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen, DeepSeek, any OpenAI-compatible endpoint
|
||||||
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
|
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
|
||||||
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
|
- **Customizable** — per-agent personas (agent TOML `system_prompt`), reusable refactor templates, full prompt-template control
|
||||||
|
|
||||||
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
||||||
|
|
||||||
@@ -157,29 +157,17 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Quick Setup (Recommended for Beginners)
|
### Quick Setup
|
||||||
|
|
||||||
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
|
QodeAssist is configured through three settings pages (Preferences > QodeAssist):
|
||||||
<details>
|
|
||||||
<summary>Quick setup: (click to expand)</summary>
|
|
||||||
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
|
|
||||||
</details>
|
|
||||||
|
|
||||||
1. **Open QodeAssist Settings**
|
1. **Providers** — pick a bundled provider instance (Claude, OpenAI, Google AI, Mistral, Ollama, …) and enter its API key. Local providers (Ollama, llama.cpp, LM Studio) need no key.
|
||||||
2. **Select a Preset** - Choose from the Quick Setup dropdown:
|
2. **Agents** — every feature runs an *agent*: a bundled TOML preset combining a provider, model, and request template. The bundled agents work out of the box; create your own by extending a base agent under a new name.
|
||||||
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
|
3. **General > Agent Pipelines** — assign agents to the four feature slots: code completion (ordered, routed per file), chat assistant (picker allow-list), chat compression, and quick refactor. Local Ollama agents are pre-assigned by default; an unassigned slot disables that feature.
|
||||||
- **OpenAI** (gpt-5.2-codex)
|
|
||||||
- **Mistral AI** (Codestral 2501)
|
|
||||||
- **Google AI** (Gemini 2.5 Flash)
|
|
||||||
- **Qwen** (Qwen3.6 Plus, Qwen3.7 Max)
|
|
||||||
- **DeepSeek** (DeepSeek V4 Flash, DeepSeek V4 Pro)
|
|
||||||
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
|
||||||
|
|
||||||
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
### Providers
|
||||||
|
|
||||||
### Manual Provider Configuration
|
Choose your preferred provider:
|
||||||
|
|
||||||
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
|
|
||||||
|
|
||||||
**Local providers:**
|
**Local providers:**
|
||||||
- **[Ollama](docs/ollama-configuration.md)** — native Ollama API
|
- **[Ollama](docs/ollama-configuration.md)** — native Ollama API
|
||||||
@@ -193,8 +181,8 @@ For advanced users or local models, choose your preferred provider and follow th
|
|||||||
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
|
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
|
||||||
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
|
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
|
||||||
- **[Google AI](docs/google-ai-configuration.md)** — Gemini
|
- **[Google AI](docs/google-ai-configuration.md)** — Gemini
|
||||||
- **Qwen (Alibaba)** — DashScope OpenAI-compatible Chat and Responses endpoints
|
- **Qwen (Alibaba)** — DashScope OpenAI-compatible endpoint (`qwen-plus`, `qwen-max`, `qwen-coder`)
|
||||||
- **DeepSeek** — `deepseek-chat` and `deepseek-reasoner` (reasoning shown as thinking)
|
- **DeepSeek** — OpenAI-compatible endpoint, `deepseek-chat` and `deepseek-reasoner`
|
||||||
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
||||||
|
|
||||||
### Recommended Models for Best Experience
|
### Recommended Models for Best Experience
|
||||||
@@ -217,9 +205,8 @@ For optimal coding assistance, we recommend using these top-tier models:
|
|||||||
|
|
||||||
### Additional Configuration
|
### Additional Configuration
|
||||||
|
|
||||||
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents (including personas via `system_prompt`) with TOML profiles
|
||||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
||||||
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
|
||||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -255,7 +242,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
|||||||
- Multiple chat panels: side panel, bottom panel, and popup window
|
- Multiple chat panels: side panel, bottom panel, and popup window
|
||||||
- Chat history with auto-save and restore
|
- Chat history with auto-save and restore
|
||||||
- Token usage monitoring
|
- Token usage monitoring
|
||||||
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
|
- AI personas via agent `system_prompt` — switch personas by switching agents (see [Creating Agents](docs/creating-agents.md))
|
||||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
||||||
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
||||||
- Automatic syncing with open editor files (optional)
|
- Automatic syncing with open editor files (optional)
|
||||||
@@ -349,18 +336,16 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │
|
│ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │
|
||||||
│ │
|
│ │
|
||||||
│ 1. System Prompt (from Code Completion Settings - FIM variant) │
|
│ 1. Editor Context: │
|
||||||
│ 2. Project Rules: │
|
│ ├─ File information (language, path) │
|
||||||
│ └─ .qodeassist/rules/completion/*.md │
|
│ ├─ Recent project changes (optional, if enabled) │
|
||||||
│ 3. Open Files Context (optional, if enabled): │
|
│ └─ Open editor files (optional, if enabled) │
|
||||||
│ └─ Currently open editor files │
|
│ 2. Code Context: │
|
||||||
│ 4. Code Context: │
|
|
||||||
│ ├─ Code before cursor (prefix) │
|
│ ├─ Code before cursor (prefix) │
|
||||||
│ └─ Code after cursor (suffix) │
|
│ └─ Code after cursor (suffix) │
|
||||||
│ │
|
│ │
|
||||||
│ Final Prompt: FIM_Template(Prefix: SystemPrompt + Rules + OpenFiles + │
|
│ Final Request: the agent's TOML [body] template renders prefix/suffix │
|
||||||
│ CodeBefore, │
|
│ into the provider's native FIM fields │
|
||||||
│ Suffix: CodeAfter) │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -375,21 +360,19 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │
|
│ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │
|
||||||
│ │
|
│ │
|
||||||
│ 1. System Prompt (from Code Completion Settings - Non-FIM variant) │
|
│ 1. Completion Instructions (from the agent's TOML profile) │
|
||||||
│ └─ Includes response formatting instructions │
|
│ └─ Includes response formatting rules │
|
||||||
│ 2. Project Rules: │
|
│ 2. Editor Context: │
|
||||||
│ └─ .qodeassist/rules/completion/*.md │
|
|
||||||
│ 3. Open Files Context (optional, if enabled): │
|
|
||||||
│ └─ Currently open editor files │
|
|
||||||
│ 4. Code Context: │
|
|
||||||
│ ├─ File information (language, path) │
|
│ ├─ File information (language, path) │
|
||||||
|
│ ├─ Recent project changes (optional, if enabled) │
|
||||||
|
│ └─ Open editor files (optional, if enabled) │
|
||||||
|
│ 3. Code Context: │
|
||||||
│ ├─ Code before cursor │
|
│ ├─ Code before cursor │
|
||||||
│ ├─ <cursor> marker │
|
│ ├─ <cursor> marker │
|
||||||
│ └─ Code after cursor │
|
│ └─ Code after cursor │
|
||||||
│ 5. User Message: "Complete the code at cursor position" │
|
|
||||||
│ │
|
│ │
|
||||||
│ Final Prompt: [System: SystemPrompt + Rules] │
|
│ Final Prompt: [System: Instructions + EditorContext] │
|
||||||
│ [User: OpenFiles + Context + CompletionRequest] │
|
│ [User: Code around cursor as a completion request] │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -402,27 +385,22 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ CHAT ASSISTANT │
|
│ CHAT ASSISTANT │
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ 1. System Prompt (from Chat Assistant Settings) │
|
│ 1. Agent System Prompt (persona, from the agent's TOML profile) │
|
||||||
│ 2. Agent Role (optional, from role selector): │
|
│ 2. Project Info + Skills (catalog and always-on skills) │
|
||||||
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
|
│ 3. Tool Definitions (if the agent enables tools) │
|
||||||
│ 3. Project Rules: │
|
│ 4. Conversation History: │
|
||||||
│ ├─ .qodeassist/rules/common/*.md │
|
│ ├─ Previous messages and tool calls/results │
|
||||||
│ └─ .qodeassist/rules/chat/*.md │
|
│ ├─ Attachments stay with the message they were sent with │
|
||||||
│ 4. File Context (optional): │
|
│ └─ /skill instructions persist for the whole conversation │
|
||||||
│ ├─ Attached files (manual) │
|
│ 5. Linked Files + Open Editor Files (if auto-sync enabled): │
|
||||||
│ ├─ Linked files (persistent) │
|
│ └─ FRESH snapshot of current file content, re-read on every │
|
||||||
│ └─ Open editor files (if auto-sync enabled) │
|
│ request and placed next to your latest message — never │
|
||||||
│ 5. Tool Definitions (if enabled): │
|
│ duplicated into the history │
|
||||||
│ ├─ ReadProjectFileByName │
|
│ 6. User Message (+ this turn's attachments and images) │
|
||||||
│ ├─ ListProjectFiles │
|
|
||||||
│ ├─ SearchInProject │
|
|
||||||
│ └─ GetIssuesList │
|
|
||||||
│ 6. Conversation History │
|
|
||||||
│ 7. User Message │
|
|
||||||
│ │
|
│ │
|
||||||
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
|
│ Final Prompt: [System: Persona + ProjectInfo + Skills] │
|
||||||
│ [History: Previous messages] │
|
│ [History: Previous messages] │
|
||||||
│ [User: FileContext + UserMessage] │
|
│ [User: CurrentFilesSnapshot + UserMessage + Attachments] │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -435,28 +413,26 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
│ QUICK REFACTORING │
|
│ QUICK REFACTORING │
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ 1. System Prompt (from Quick Refactor Settings) │
|
│ 1. Agent System Prompt (persona, from the agent's TOML profile) │
|
||||||
│ 2. Project Rules: │
|
│ 2. Code Context (generated): │
|
||||||
│ ├─ .qodeassist/rules/common/*.md │
|
|
||||||
│ └─ .qodeassist/rules/quickrefactor/*.md │
|
|
||||||
│ 3. Code Context: │
|
|
||||||
│ ├─ File information (language, path) │
|
│ ├─ File information (language, path) │
|
||||||
│ ├─ Code before selection (configurable amount) │
|
│ ├─ Code before selection (configurable amount) │
|
||||||
│ ├─ <selection_start> marker │
|
│ ├─ <selection_start> marker │
|
||||||
│ ├─ Selected code (or current line) │
|
│ ├─ Selected code (or current line) │
|
||||||
│ ├─ <selection_end> marker │
|
│ ├─ <selection_end> marker │
|
||||||
│ ├─ <cursor> marker (position within selection) │
|
│ ├─ <cursor> marker (position within selection) │
|
||||||
│ └─ Code after selection (configurable amount) │
|
│ ├─ Code after selection (configurable amount) │
|
||||||
│ 4. Refactor Instruction: │
|
│ └─ Output formatting and indentation rules │
|
||||||
|
│ 3. Refactor Instruction (the user message): │
|
||||||
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
|
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
|
||||||
│ ├─ Custom Instruction (from library) │
|
│ ├─ Custom Instruction (from library) │
|
||||||
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
|
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
|
||||||
│ │ quick_refactor/instructions/*.json │
|
│ │ quick_refactor/instructions/*.json │
|
||||||
│ └─ Additional Details (optional user input) │
|
│ └─ Additional Details (optional user input) │
|
||||||
│ 5. Tool Definitions (if enabled) │
|
│ 4. Tool Definitions (if the agent enables tools) │
|
||||||
│ │
|
│ │
|
||||||
│ Final Prompt: [System: SystemPrompt + Rules] │
|
│ Final Prompt: [System: Persona + CodeContext + Rules] │
|
||||||
│ [User: Context + Markers + Instruction + Details] │
|
│ [User: Instruction + Details] │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -464,17 +440,15 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
|
|
||||||
### Key Points
|
### Key Points
|
||||||
|
|
||||||
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
- **System Prompts** live in the agent's TOML profile (`system_prompt`); switch personas by switching agents
|
||||||
- **System Prompts** are configured independently for each feature in Settings
|
- **Linked and open-synced files are always current**: their content is not stored in the conversation — every request re-reads the files and sends a fresh snapshot next to your latest message. Editing a linked file between messages never leaves a stale copy in the context, and changing it does not invalidate the provider's prompt cache for the whole conversation
|
||||||
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
|
- **One-time attachments are different**: they are saved with the message they were sent with and stay in the history as sent
|
||||||
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
- **FIM vs Non-FIM** for code completion is the agent's choice: a FIM agent renders prefix/suffix into native FIM fields, an instruct agent sends a chat-shaped request — pick the agent that matches your model
|
||||||
- FIM models: Direct completion prompt
|
- **Quick Refactor** has its own agent roster, independent from Chat
|
||||||
- Non-FIM models: Prompt includes response formatting instructions
|
|
||||||
- **Quick Refactor** has its own provider/model configuration, independent from Chat
|
|
||||||
- **Custom Instructions** provide reusable templates that can be augmented with specific details
|
- **Custom Instructions** provide reusable templates that can be augmented with specific details
|
||||||
- **Tool Calling** is available for Chat and Quick Refactor when enabled
|
- **Tool Calling** is available for Chat and Quick Refactor when the agent enables it; tool rounds per request are limited (configurable in `Settings → Tools`)
|
||||||
|
|
||||||
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
See the [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
@@ -522,7 +496,6 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
|
|||||||
- [x] Diff sharing with models
|
- [x] Diff sharing with models
|
||||||
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
||||||
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
|
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
|
||||||
- [x] Project-specific rules (`.qodeassist/rules/`)
|
|
||||||
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
||||||
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
||||||
- [ ] Full project source sharing
|
- [ ] Full project source sharing
|
||||||
@@ -533,7 +506,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
|||||||
|
|
||||||
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
|
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
|
||||||
|
|
||||||
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features.
|
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||||
|
|
||||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||||
|
|
||||||
@@ -581,6 +554,10 @@ cmake --build .
|
|||||||
|
|
||||||
## For Contributors
|
## For Contributors
|
||||||
|
|
||||||
|
### Adding an agent preset
|
||||||
|
|
||||||
|
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
|
||||||
@@ -589,7 +566,7 @@ cmake --build .
|
|||||||
|
|
||||||
### Development Guidelines
|
### Development Guidelines
|
||||||
|
|
||||||
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
|
For detailed development guidelines and architecture patterns, see [docs/architecture.md](docs/architecture.md) and [docs/target-architecture.md](docs/target-architecture.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ public:
|
|||||||
|
|
||||||
void setSuggestionRange(const Utils::Text::Range &range);
|
void setSuggestionRange(const Utils::Text::Range &range);
|
||||||
void clearSuggestionRange();
|
void clearSuggestionRange();
|
||||||
bool hasSuggestion() const { return m_hasSuggestion; }
|
|
||||||
|
|
||||||
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
|
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
|
||||||
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }
|
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }
|
||||||
|
|||||||
@@ -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,105 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowEditor.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
FlowEditor::FlowEditor(QQuickItem *parent)
|
|
||||||
: QQuickItem(parent)
|
|
||||||
{}
|
|
||||||
|
|
||||||
void FlowEditor::initialize()
|
|
||||||
{
|
|
||||||
emit availableTaskTypesChanged();
|
|
||||||
emit availableFlowsChanged();
|
|
||||||
|
|
||||||
m_flowsModel = new FlowsModel(m_flowManager, this);
|
|
||||||
|
|
||||||
emit flowsModelChanged();
|
|
||||||
|
|
||||||
if (m_flowsModel->rowCount() > 0) {
|
|
||||||
setCurrentFlowIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// setCurrentFlowId(m_flowManager->flows().begin().value()->flowId());
|
|
||||||
m_currentFlow = m_flowManager->getFlow();
|
|
||||||
emit currentFlowChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FlowEditor::currentFlowId() const
|
|
||||||
{
|
|
||||||
return m_currentFlowId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowEditor::setCurrentFlowId(const QString &newCurrentFlowId)
|
|
||||||
{
|
|
||||||
if (m_currentFlowId == newCurrentFlowId)
|
|
||||||
return;
|
|
||||||
m_currentFlowId = newCurrentFlowId;
|
|
||||||
emit currentFlowIdChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList FlowEditor::availableTaskTypes() const
|
|
||||||
{
|
|
||||||
if (m_flowManager)
|
|
||||||
return m_flowManager->getAvailableTasksTypes();
|
|
||||||
else {
|
|
||||||
return {"No flow manager"};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList FlowEditor::availableFlows() const
|
|
||||||
{
|
|
||||||
if (m_flowManager) {
|
|
||||||
auto flows = m_flowManager->getAvailableFlows();
|
|
||||||
return flows.size() > 0 ? flows : QStringList{"No flows"};
|
|
||||||
} else {
|
|
||||||
return {"No flow manager"};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowEditor::setFlowManager(FlowManager *newFlowManager)
|
|
||||||
{
|
|
||||||
if (m_flowManager == newFlowManager)
|
|
||||||
return;
|
|
||||||
m_flowManager = newFlowManager;
|
|
||||||
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowsModel *FlowEditor::flowsModel() const
|
|
||||||
{
|
|
||||||
return m_flowsModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowEditor::currentFlowIndex() const
|
|
||||||
{
|
|
||||||
return m_currentFlowIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowEditor::setCurrentFlowIndex(int newCurrentFlowIndex)
|
|
||||||
{
|
|
||||||
if (m_currentFlowIndex == newCurrentFlowIndex)
|
|
||||||
return;
|
|
||||||
m_currentFlowIndex = newCurrentFlowIndex;
|
|
||||||
emit currentFlowIndexChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow *FlowEditor::getFlow(const QString &flowName)
|
|
||||||
{
|
|
||||||
return m_flowManager->getFlow(flowName);
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow *FlowEditor::getCurrentFlow()
|
|
||||||
{
|
|
||||||
return m_flowManager->getFlow(m_currentFlowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow *FlowEditor::currentFlow() const
|
|
||||||
{
|
|
||||||
return m_currentFlow;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QQuickItem>
|
|
||||||
|
|
||||||
#include "FlowsModel.hpp"
|
|
||||||
#include <FlowManager.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class FlowEditor : public QQuickItem
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(
|
|
||||||
QString currentFlowId READ currentFlowId WRITE setCurrentFlowId NOTIFY currentFlowIdChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
QStringList availableTaskTypes READ availableTaskTypes NOTIFY availableTaskTypesChanged)
|
|
||||||
Q_PROPERTY(QStringList availableFlows READ availableFlows NOTIFY availableFlowsChanged)
|
|
||||||
Q_PROPERTY(FlowsModel *flowsModel READ flowsModel NOTIFY flowsModelChanged)
|
|
||||||
Q_PROPERTY(int currentFlowIndex READ currentFlowIndex WRITE setCurrentFlowIndex NOTIFY
|
|
||||||
currentFlowIndexChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(Flow *currentFlow READ currentFlow NOTIFY currentFlowChanged FINAL)
|
|
||||||
|
|
||||||
public:
|
|
||||||
FlowEditor(QQuickItem *parent = nullptr);
|
|
||||||
|
|
||||||
void initialize();
|
|
||||||
|
|
||||||
QString currentFlowId() const;
|
|
||||||
void setCurrentFlowId(const QString &newCurrentFlowId);
|
|
||||||
|
|
||||||
QStringList availableTaskTypes() const;
|
|
||||||
QStringList availableFlows() const;
|
|
||||||
|
|
||||||
void setFlowManager(FlowManager *newFlowManager);
|
|
||||||
|
|
||||||
FlowsModel *flowsModel() const;
|
|
||||||
|
|
||||||
int currentFlowIndex() const;
|
|
||||||
void setCurrentFlowIndex(int newCurrentFlowIndex);
|
|
||||||
|
|
||||||
Q_INVOKABLE Flow *getFlow(const QString &flowName);
|
|
||||||
Q_INVOKABLE Flow *getCurrentFlow();
|
|
||||||
|
|
||||||
Flow *currentFlow() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void currentFlowIdChanged();
|
|
||||||
void availableTaskTypesChanged();
|
|
||||||
void availableFlowsChanged();
|
|
||||||
void flowsModelChanged();
|
|
||||||
|
|
||||||
void currentFlowIndexChanged();
|
|
||||||
|
|
||||||
void currentFlowChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
FlowManager *m_flowManager = nullptr;
|
|
||||||
QString m_currentFlowId;
|
|
||||||
FlowsModel *m_flowsModel;
|
|
||||||
int m_currentFlowIndex;
|
|
||||||
Flow *m_currentFlow = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowItem.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
FlowItem::FlowItem(QQuickItem *parent)
|
|
||||||
: QQuickItem(parent)
|
|
||||||
{
|
|
||||||
connect(this, &QQuickItem::childrenChanged, this, [this]() { updateFlowLayout(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FlowItem::flowId() const
|
|
||||||
{
|
|
||||||
if (!m_flow)
|
|
||||||
return {"no flow"};
|
|
||||||
return m_flow->flowId();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowItem::setFlowId(const QString &newFlowId)
|
|
||||||
{
|
|
||||||
if (m_flow->flowId() == newFlowId)
|
|
||||||
return;
|
|
||||||
m_flow->setFlowId(newFlowId);
|
|
||||||
emit flowIdChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow *FlowItem::flow() const
|
|
||||||
{
|
|
||||||
return m_flow;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowItem::setFlow(Flow *newFlow)
|
|
||||||
{
|
|
||||||
if (m_flow == newFlow)
|
|
||||||
return;
|
|
||||||
m_flow = newFlow;
|
|
||||||
emit flowChanged();
|
|
||||||
emit flowIdChanged();
|
|
||||||
qDebug() << "FlowItem::setFlow" << m_flow->flowId() << newFlow;
|
|
||||||
|
|
||||||
m_taskModel = new TaskModel(m_flow, this);
|
|
||||||
m_connectionsModel = new TaskConnectionsModel(m_flow, this);
|
|
||||||
|
|
||||||
emit taskModelChanged();
|
|
||||||
emit connectionsModelChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskModel *FlowItem::taskModel() const
|
|
||||||
{
|
|
||||||
return m_taskModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskConnectionsModel *FlowItem::connectionsModel() const
|
|
||||||
{
|
|
||||||
return m_connectionsModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList FlowItem::taskItems() const
|
|
||||||
{
|
|
||||||
return m_taskItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowItem::setTaskItems(const QVariantList &newTaskItems)
|
|
||||||
{
|
|
||||||
qDebug() << "FlowItem::setTaskItems" << newTaskItems;
|
|
||||||
if (m_taskItems == newTaskItems)
|
|
||||||
return;
|
|
||||||
m_taskItems = newTaskItems;
|
|
||||||
emit taskItemsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowItem::updateFlowLayout()
|
|
||||||
{
|
|
||||||
auto allItems = this->childItems();
|
|
||||||
|
|
||||||
for (auto child : allItems) {
|
|
||||||
if (child->objectName() == QString("TaskItem")) {
|
|
||||||
qDebug() << "Found TaskItem:" << child;
|
|
||||||
auto taskItem = qobject_cast<TaskItem *>(child);
|
|
||||||
m_taskItemsList.insert(taskItem, taskItem->task());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (child->objectName() == QString("TaskConnectionItem")) {
|
|
||||||
qDebug() << "Found TaskConnectionItem:" << child;
|
|
||||||
auto connectionItem = qobject_cast<TaskConnectionItem *>(child);
|
|
||||||
m_taskConnectionsList.insert(connectionItem, connectionItem->connection());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QQuickItem>
|
|
||||||
|
|
||||||
#include "TaskConnectionItem.hpp"
|
|
||||||
#include "TaskConnectionsModel.hpp"
|
|
||||||
#include "TaskItem.hpp"
|
|
||||||
#include "TaskModel.hpp"
|
|
||||||
#include <Flow.hpp>
|
|
||||||
#include <TaskConnection.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class FlowItem : public QQuickItem
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(QString flowId READ flowId WRITE setFlowId NOTIFY flowIdChanged)
|
|
||||||
Q_PROPERTY(Flow *flow READ flow WRITE setFlow NOTIFY flowChanged)
|
|
||||||
Q_PROPERTY(TaskModel *taskModel READ taskModel NOTIFY taskModelChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
TaskConnectionsModel *connectionsModel READ connectionsModel NOTIFY connectionsModelChanged)
|
|
||||||
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit FlowItem(QQuickItem *parent = nullptr);
|
|
||||||
|
|
||||||
QString flowId() const;
|
|
||||||
void setFlowId(const QString &newFlowId);
|
|
||||||
|
|
||||||
Flow *flow() const;
|
|
||||||
void setFlow(Flow *newFlow);
|
|
||||||
|
|
||||||
TaskModel *taskModel() const;
|
|
||||||
|
|
||||||
TaskConnectionsModel *connectionsModel() const;
|
|
||||||
|
|
||||||
QVariantList taskItems() const;
|
|
||||||
void setTaskItems(const QVariantList &newTaskItems);
|
|
||||||
|
|
||||||
void updateFlowLayout();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void flowIdChanged();
|
|
||||||
void flowChanged();
|
|
||||||
void taskModelChanged();
|
|
||||||
void connectionsModelChanged();
|
|
||||||
void taskItemsChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
Flow *m_flow = nullptr;
|
|
||||||
TaskModel *m_taskModel = nullptr;
|
|
||||||
TaskConnectionsModel *m_connectionsModel = nullptr;
|
|
||||||
QVariantList m_taskItems;
|
|
||||||
|
|
||||||
QHash<TaskItem *, BaseTask *> m_taskItemsList;
|
|
||||||
QHash<TaskConnectionItem *, TaskConnection *> m_taskConnectionsList;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowsModel.hpp"
|
|
||||||
|
|
||||||
#include "FlowManager.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
FlowsModel::FlowsModel(FlowManager *flowManager, QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
, m_flowManager(flowManager)
|
|
||||||
{
|
|
||||||
connect(m_flowManager, &FlowManager::flowAdded, this, &FlowsModel::onFlowAdded);
|
|
||||||
}
|
|
||||||
|
|
||||||
int FlowsModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return m_flowManager->flows().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant FlowsModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (!index.isValid() || !m_flowManager || index.row() >= m_flowManager->flows().size())
|
|
||||||
return QVariant();
|
|
||||||
|
|
||||||
const auto flows = m_flowManager->flows().values();
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case FlowRoles::FlowIdRole:
|
|
||||||
return flows.at(index.row())->flowId();
|
|
||||||
case FlowRoles::FlowDataRole:
|
|
||||||
return QVariant::fromValue(flows.at(index.row()));
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> FlowsModel::roleNames() const
|
|
||||||
{
|
|
||||||
QHash<int, QByteArray> roles;
|
|
||||||
roles[FlowRoles::FlowIdRole] = "flowId";
|
|
||||||
roles[FlowRoles::FlowDataRole] = "flowData";
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowsModel::onFlowAdded(const QString &flowId)
|
|
||||||
{
|
|
||||||
// qDebug() << "FlowsModel::Flow added: " << flowId;
|
|
||||||
// int newIndex = m_flowManager->flows().size();
|
|
||||||
// beginInsertRows(QModelIndex(), newIndex, newIndex);
|
|
||||||
// endInsertRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowsModel::onFlowRemoved(const QString &flowId) {}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
// #include "tasks/Flow.hpp"
|
|
||||||
#include <FlowManager.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class FlowsModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
enum FlowRoles { FlowIdRole = Qt::UserRole, FlowDataRole };
|
|
||||||
|
|
||||||
FlowsModel(FlowManager *flowManager, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void onFlowAdded(const QString &flowId);
|
|
||||||
void onFlowRemoved(const QString &flowId);
|
|
||||||
|
|
||||||
private:
|
|
||||||
FlowManager *m_flowManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "GridBackground.hpp"
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QQuickWindow>
|
|
||||||
#include <QSGSimpleRectNode>
|
|
||||||
#include <QSGSimpleTextureNode>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
GridBackground::GridBackground(QQuickItem *parent)
|
|
||||||
: QQuickItem(parent)
|
|
||||||
{
|
|
||||||
setFlag(QQuickItem::ItemHasContents, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
int GridBackground::gridSize() const
|
|
||||||
{
|
|
||||||
return m_gridSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GridBackground::setGridSize(int size)
|
|
||||||
{
|
|
||||||
if (m_gridSize != size) {
|
|
||||||
m_gridSize = size;
|
|
||||||
update();
|
|
||||||
emit gridSizeChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor GridBackground::gridColor() const
|
|
||||||
{
|
|
||||||
return m_gridColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void GridBackground::setGridColor(const QColor &color)
|
|
||||||
{
|
|
||||||
if (m_gridColor != color) {
|
|
||||||
m_gridColor = color;
|
|
||||||
update();
|
|
||||||
emit gridColorChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QSGNode *GridBackground::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
|
|
||||||
{
|
|
||||||
QSGSimpleTextureNode *node = static_cast<QSGSimpleTextureNode *>(oldNode);
|
|
||||||
if (!node) {
|
|
||||||
node = new QSGSimpleTextureNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
QPixmap pixmap(width(), height());
|
|
||||||
pixmap.fill(Qt::transparent);
|
|
||||||
|
|
||||||
QPainter painter(&pixmap);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
|
||||||
|
|
||||||
QPen pen(m_gridColor);
|
|
||||||
pen.setWidth(1);
|
|
||||||
painter.setPen(pen);
|
|
||||||
painter.setOpacity(this->opacity());
|
|
||||||
|
|
||||||
for (int x = 0; x < width(); x += m_gridSize) {
|
|
||||||
painter.drawLine(x, 0, x, height());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int y = 0; y < height(); y += m_gridSize) {
|
|
||||||
painter.drawLine(0, y, width(), y);
|
|
||||||
}
|
|
||||||
|
|
||||||
painter.end();
|
|
||||||
|
|
||||||
QSGTexture *texture = window()->createTextureFromImage(pixmap.toImage());
|
|
||||||
node->setTexture(texture);
|
|
||||||
node->setRect(boundingRect());
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QColor>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QQuickItem>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class GridBackground : public QQuickItem
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
|
|
||||||
Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit GridBackground(QQuickItem *parent = nullptr);
|
|
||||||
|
|
||||||
int gridSize() const;
|
|
||||||
void setGridSize(int size);
|
|
||||||
|
|
||||||
QColor gridColor() const;
|
|
||||||
void setGridColor(const QColor &color);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void gridSizeChanged();
|
|
||||||
void gridColorChanged();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
int m_gridSize = 20;
|
|
||||||
QColor m_gridColor = QColor(128, 128, 128);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskConnectionItem.hpp"
|
|
||||||
#include "TaskItem.hpp"
|
|
||||||
#include "TaskPortItem.hpp"
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskConnectionItem::TaskConnectionItem(QQuickItem *parent)
|
|
||||||
: QQuickItem(parent)
|
|
||||||
{
|
|
||||||
setObjectName("TaskConnectionItem");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskConnectionItem::setConnection(TaskConnection *connection)
|
|
||||||
{
|
|
||||||
if (m_connection == connection)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_connection = connection;
|
|
||||||
emit connectionChanged();
|
|
||||||
|
|
||||||
calculatePositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskConnectionItem::updatePositions()
|
|
||||||
{
|
|
||||||
// calculatePositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskConnectionItem::calculatePositions()
|
|
||||||
{
|
|
||||||
if (!m_connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find source task item
|
|
||||||
QQuickItem *sourceTaskItem = findTaskItem(m_connection->sourceTask());
|
|
||||||
QQuickItem *targetTaskItem = findTaskItem(m_connection->targetTask());
|
|
||||||
|
|
||||||
if (!sourceTaskItem || !targetTaskItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find port items within tasks
|
|
||||||
QQuickItem *sourcePortItem = findPortItem(sourceTaskItem, m_connection->sourcePort());
|
|
||||||
QQuickItem *targetPortItem = findPortItem(targetTaskItem, m_connection->targetPort());
|
|
||||||
|
|
||||||
if (!sourcePortItem || !targetPortItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate global positions
|
|
||||||
QPointF sourceGlobal
|
|
||||||
= sourcePortItem
|
|
||||||
->mapToItem(parentItem(), sourcePortItem->width() / 2, sourcePortItem->height() / 2);
|
|
||||||
QPointF targetGlobal
|
|
||||||
= targetPortItem
|
|
||||||
->mapToItem(parentItem(), targetPortItem->width() / 2, targetPortItem->height() / 2);
|
|
||||||
|
|
||||||
if (m_startPoint != sourceGlobal) {
|
|
||||||
m_startPoint = sourceGlobal;
|
|
||||||
emit startPointChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_endPoint != targetGlobal) {
|
|
||||||
m_endPoint = targetGlobal;
|
|
||||||
emit endPointChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickItem *TaskConnectionItem::findTaskItem(BaseTask *task)
|
|
||||||
{
|
|
||||||
for (const QVariant &item : m_taskItems) {
|
|
||||||
QQuickItem *taskItem = qvariant_cast<QQuickItem *>(item);
|
|
||||||
if (!taskItem)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
QVariant taskProp = taskItem->property("task");
|
|
||||||
if (taskProp.isValid() && taskProp.value<BaseTask *>() == task) {
|
|
||||||
return taskItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickItem *TaskConnectionItem::findTaskItemRecursive(QQuickItem *item, BaseTask *task)
|
|
||||||
{
|
|
||||||
// Проверяем objectName и task property
|
|
||||||
if (item->objectName() == "TaskItem") {
|
|
||||||
QVariant taskProp = item->property("task");
|
|
||||||
if (taskProp.isValid()) {
|
|
||||||
BaseTask *itemTask = taskProp.value<BaseTask *>();
|
|
||||||
if (itemTask == task) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Рекурсивно ищем в детях
|
|
||||||
auto children = item->childItems();
|
|
||||||
|
|
||||||
for (QQuickItem *child : children) {
|
|
||||||
if (QQuickItem *found = findTaskItemRecursive(child, task)) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickItem *TaskConnectionItem::findPortItem(QQuickItem *taskItem, TaskPort *port)
|
|
||||||
{
|
|
||||||
std::function<QQuickItem *(QQuickItem *)> findPortRecursive =
|
|
||||||
[&](QQuickItem *item) -> QQuickItem * {
|
|
||||||
// Проверяем objectName и port property
|
|
||||||
if (item->objectName() == "TaskPortItem") {
|
|
||||||
QVariant portProp = item->property("port");
|
|
||||||
if (portProp.isValid()) {
|
|
||||||
TaskPort *itemPort = portProp.value<TaskPort *>();
|
|
||||||
if (itemPort == port) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Рекурсивно ищем в детях
|
|
||||||
for (QQuickItem *child : item->childItems()) {
|
|
||||||
if (QQuickItem *found = findPortRecursive(child)) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
return findPortRecursive(taskItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantList TaskConnectionItem::taskItems() const
|
|
||||||
{
|
|
||||||
return m_taskItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskConnectionItem::setTaskItems(const QVariantList &newTaskItems)
|
|
||||||
{
|
|
||||||
if (m_taskItems == newTaskItems)
|
|
||||||
return;
|
|
||||||
m_taskItems = newTaskItems;
|
|
||||||
emit taskItemsChanged();
|
|
||||||
|
|
||||||
calculatePositions();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "TaskConnection.hpp"
|
|
||||||
#include <QPointF>
|
|
||||||
#include <QQuickItem>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskConnectionItem : public QQuickItem
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(QPointF startPoint READ startPoint NOTIFY startPointChanged)
|
|
||||||
Q_PROPERTY(QPointF endPoint READ endPoint NOTIFY endPointChanged)
|
|
||||||
Q_PROPERTY(
|
|
||||||
TaskConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
|
|
||||||
|
|
||||||
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
TaskConnectionItem(QQuickItem *parent = nullptr);
|
|
||||||
|
|
||||||
QPointF startPoint() const { return m_startPoint; }
|
|
||||||
QPointF endPoint() const { return m_endPoint; }
|
|
||||||
|
|
||||||
TaskConnection *connection() const { return m_connection; }
|
|
||||||
void setConnection(TaskConnection *connection);
|
|
||||||
|
|
||||||
Q_INVOKABLE void updatePositions();
|
|
||||||
|
|
||||||
QVariantList taskItems() const;
|
|
||||||
void setTaskItems(const QVariantList &newTaskItems);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void startPointChanged();
|
|
||||||
void endPointChanged();
|
|
||||||
void connectionChanged();
|
|
||||||
|
|
||||||
void taskItemsChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void calculatePositions();
|
|
||||||
QQuickItem *findTaskItem(BaseTask *task);
|
|
||||||
QQuickItem *findTaskItemRecursive(QQuickItem *item, BaseTask *task);
|
|
||||||
QQuickItem *findPortItem(QQuickItem *taskItem, TaskPort *port);
|
|
||||||
|
|
||||||
private:
|
|
||||||
TaskConnection *m_connection = nullptr;
|
|
||||||
QPointF m_startPoint;
|
|
||||||
QPointF m_endPoint;
|
|
||||||
QVariantList m_taskItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskConnectionsModel.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskConnectionsModel::TaskConnectionsModel(Flow *flow, QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
, m_flow(flow)
|
|
||||||
{}
|
|
||||||
|
|
||||||
int TaskConnectionsModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return m_flow->connections().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant TaskConnectionsModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (role == TaskConnectionsRoles::TaskConnectionsRole)
|
|
||||||
return QVariant::fromValue(m_flow->connections().at(index.row()));
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> TaskConnectionsModel::roleNames() const
|
|
||||||
{
|
|
||||||
QHash<int, QByteArray> roles;
|
|
||||||
roles[TaskConnectionsRoles::TaskConnectionsRole] = "connectionData";
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include <Flow.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskConnectionsModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
enum TaskConnectionsRoles { TaskConnectionsRole = Qt::UserRole };
|
|
||||||
|
|
||||||
explicit TaskConnectionsModel(Flow *flow, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Flow *m_flow;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskItem.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskItem::TaskItem(QQuickItem *parent)
|
|
||||||
: QQuickItem(parent)
|
|
||||||
{
|
|
||||||
setObjectName("TaskItem");
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TaskItem::taskId() const
|
|
||||||
{
|
|
||||||
return m_taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskItem::setTaskId(const QString &newTaskId)
|
|
||||||
{
|
|
||||||
if (m_taskId == newTaskId)
|
|
||||||
return;
|
|
||||||
m_taskId = newTaskId;
|
|
||||||
emit taskIdChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TaskItem::taskType() const
|
|
||||||
{
|
|
||||||
return m_task ? m_task->taskType() : QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseTask *TaskItem::task() const
|
|
||||||
{
|
|
||||||
return m_task;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskItem::setTask(BaseTask *newTask)
|
|
||||||
{
|
|
||||||
if (m_task == newTask)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_task = newTask;
|
|
||||||
|
|
||||||
if (m_task) {
|
|
||||||
m_taskId = m_task->taskId();
|
|
||||||
|
|
||||||
// Обновляем модели портов
|
|
||||||
m_inputPorts = new TaskPortModel(m_task->getInputPorts(), this);
|
|
||||||
m_outputPorts = new TaskPortModel(m_task->getOutputPorts(), this);
|
|
||||||
} else {
|
|
||||||
m_inputPorts = nullptr;
|
|
||||||
m_outputPorts = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit taskChanged();
|
|
||||||
emit inputPortsChanged();
|
|
||||||
emit outputPortsChanged();
|
|
||||||
emit taskIdChanged();
|
|
||||||
emit taskTypeChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPortModel *TaskItem::inputPorts() const
|
|
||||||
{
|
|
||||||
return m_inputPorts;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPortModel *TaskItem::outputPorts() const
|
|
||||||
{
|
|
||||||
return m_outputPorts;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QQuickItem>
|
|
||||||
|
|
||||||
#include "TaskPortModel.hpp"
|
|
||||||
#include <BaseTask.hpp>
|
|
||||||
#include <TaskPort.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskItem : public QQuickItem
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(QString taskId READ taskId WRITE setTaskId NOTIFY taskIdChanged)
|
|
||||||
Q_PROPERTY(QString taskType READ taskType NOTIFY taskTypeChanged)
|
|
||||||
Q_PROPERTY(BaseTask *task READ task WRITE setTask NOTIFY taskChanged)
|
|
||||||
Q_PROPERTY(TaskPortModel *inputPorts READ inputPorts NOTIFY inputPortsChanged)
|
|
||||||
Q_PROPERTY(TaskPortModel *outputPorts READ outputPorts NOTIFY outputPortsChanged)
|
|
||||||
|
|
||||||
public:
|
|
||||||
TaskItem(QQuickItem *parent = nullptr);
|
|
||||||
|
|
||||||
QString taskId() const;
|
|
||||||
void setTaskId(const QString &newTaskId);
|
|
||||||
QString taskType() const;
|
|
||||||
|
|
||||||
BaseTask *task() const;
|
|
||||||
void setTask(BaseTask *newTask);
|
|
||||||
|
|
||||||
TaskPortModel *inputPorts() const;
|
|
||||||
TaskPortModel *outputPorts() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void taskIdChanged();
|
|
||||||
void taskTypeChanged();
|
|
||||||
void taskChanged();
|
|
||||||
void inputPortsChanged();
|
|
||||||
void outputPortsChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_taskId;
|
|
||||||
BaseTask *m_task = nullptr;
|
|
||||||
TaskPortModel *m_inputPorts = nullptr;
|
|
||||||
TaskPortModel *m_outputPorts = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskModel.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskModel::TaskModel(Flow *flow, QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
, m_flow(flow)
|
|
||||||
{}
|
|
||||||
|
|
||||||
int TaskModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return m_flow->tasks().size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant TaskModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (!index.isValid() || !m_flow || index.row() >= m_flow->tasks().size())
|
|
||||||
return QVariant();
|
|
||||||
|
|
||||||
const auto &task = m_flow->tasks().values();
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case TaskRoles::TaskIdRole:
|
|
||||||
return task.at(index.row())->taskId();
|
|
||||||
case TaskRoles::TaskDataRole:
|
|
||||||
return QVariant::fromValue(task.at(index.row()));
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> TaskModel::roleNames() const
|
|
||||||
{
|
|
||||||
QHash<int, QByteArray> roles;
|
|
||||||
roles[TaskRoles::TaskIdRole] = "taskId";
|
|
||||||
roles[TaskRoles::TaskDataRole] = "taskData";
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
#include <Flow.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
enum TaskRoles { TaskIdRole = Qt::UserRole, TaskDataRole };
|
|
||||||
|
|
||||||
TaskModel(Flow *flow, QObject *parent);
|
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Flow *m_flow;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskPortItem.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskPortItem::TaskPortItem(QQuickItem *parent)
|
|
||||||
: QQuickItem(parent)
|
|
||||||
{
|
|
||||||
setObjectName("TaskPortItem");
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPort *TaskPortItem::port() const
|
|
||||||
{
|
|
||||||
return m_port;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskPortItem::setPort(TaskPort *newPort)
|
|
||||||
{
|
|
||||||
if (m_port == newPort)
|
|
||||||
return;
|
|
||||||
m_port = newPort;
|
|
||||||
emit portChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TaskPortItem::name() const
|
|
||||||
{
|
|
||||||
return m_port->name();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <TaskPort.hpp>
|
|
||||||
#include <QQuickItem>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskPortItem : public QQuickItem
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
QML_ELEMENT
|
|
||||||
|
|
||||||
Q_PROPERTY(TaskPort *port READ port WRITE setPort NOTIFY portChanged)
|
|
||||||
Q_PROPERTY(QString name READ name CONSTANT)
|
|
||||||
|
|
||||||
public:
|
|
||||||
TaskPortItem(QQuickItem *parent = nullptr);
|
|
||||||
|
|
||||||
TaskPort *port() const;
|
|
||||||
void setPort(TaskPort *newPort);
|
|
||||||
|
|
||||||
QString name() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void portChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
TaskPort *m_port = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskPortModel.hpp"
|
|
||||||
#include "TaskPort.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskPortModel::TaskPortModel(const QList<TaskPort *> &ports, QObject *parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
, m_ports(ports)
|
|
||||||
{}
|
|
||||||
|
|
||||||
int TaskPortModel::rowCount(const QModelIndex &parent) const
|
|
||||||
{
|
|
||||||
return m_ports.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant TaskPortModel::data(const QModelIndex &index, int role) const
|
|
||||||
{
|
|
||||||
if (!index.isValid() || index.row() >= m_ports.size())
|
|
||||||
return QVariant();
|
|
||||||
|
|
||||||
switch (role) {
|
|
||||||
case TaskPortRoles::TaskPortNameRole:
|
|
||||||
return m_ports.at(index.row())->name();
|
|
||||||
case TaskPortRoles::TaskPortDataRole:
|
|
||||||
return QVariant::fromValue(m_ports.at(index.row()));
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<int, QByteArray> TaskPortModel::roleNames() const
|
|
||||||
{
|
|
||||||
QHash<int, QByteArray> roles;
|
|
||||||
roles[TaskPortRoles::TaskPortNameRole] = "taskPortName";
|
|
||||||
roles[TaskPortRoles::TaskPortDataRole] = "taskPortData";
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
|
||||||
|
|
||||||
#include <BaseTask.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskPortModel : public QAbstractListModel
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
enum TaskPortRoles { TaskPortNameRole = Qt::UserRole, TaskPortDataRole };
|
|
||||||
|
|
||||||
TaskPortModel(const QList<TaskPort *> &ports, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QList<TaskPort *> m_ports;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import TaskFlow.Editor
|
|
||||||
|
|
||||||
FlowItem {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: tasks
|
|
||||||
|
|
||||||
model: root.taskModel
|
|
||||||
delegate: Task {
|
|
||||||
// task: taskData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: connections
|
|
||||||
|
|
||||||
model: root.taskModel
|
|
||||||
delegate: TaskConnection {
|
|
||||||
// task: taskData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// property var qtaskItems: []
|
|
||||||
|
|
||||||
// // Flow container background
|
|
||||||
// Rectangle {
|
|
||||||
// anchors.fill: parent
|
|
||||||
// color: palette.alternateBase
|
|
||||||
// border.color: palette.mid
|
|
||||||
// border.width: 2
|
|
||||||
// radius: 8
|
|
||||||
|
|
||||||
// // Flow header
|
|
||||||
// Rectangle {
|
|
||||||
// id: flowHeader
|
|
||||||
// anchors.top: parent.top
|
|
||||||
// anchors.left: parent.left
|
|
||||||
// anchors.right: parent.right
|
|
||||||
// height: 40
|
|
||||||
// color: palette.button
|
|
||||||
// radius: 6
|
|
||||||
|
|
||||||
// Rectangle {
|
|
||||||
// anchors.bottom: parent.bottom
|
|
||||||
// anchors.left: parent.left
|
|
||||||
// anchors.right: parent.right
|
|
||||||
// height: parent.radius
|
|
||||||
// color: parent.color
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Text {
|
|
||||||
// anchors.centerIn: parent
|
|
||||||
// text: root.flowId
|
|
||||||
// color: palette.buttonText
|
|
||||||
// font.pixelSize: 14
|
|
||||||
// font.bold: true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // // Tasks container
|
|
||||||
// // Row {
|
|
||||||
// // id: tasksRow
|
|
||||||
// // anchors.top: flowHeader.bottom
|
|
||||||
// // anchors.left: parent.left
|
|
||||||
// // anchors.margins: 25
|
|
||||||
// // anchors.topMargin: 25
|
|
||||||
// // objectName: "FlowTaskRow"
|
|
||||||
|
|
||||||
// // spacing: 40
|
|
||||||
|
|
||||||
// // Repeater {
|
|
||||||
// // model: root.taskModel
|
|
||||||
|
|
||||||
// // delegate: Task {
|
|
||||||
// // task: taskData
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // onItemAdded: function(index, item){
|
|
||||||
// // console.log("task added", index, item)
|
|
||||||
// // qtaskItems.push(item)
|
|
||||||
// // root.insertTaskItem(index, item)
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // onItemRemoved: function(index, item){
|
|
||||||
// // console.log("task added", index, item)
|
|
||||||
// // var idx = qtaskItems.indexOf(item)
|
|
||||||
// // if (idx !== -1) qtaskItems.splice(idx, 1)
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // Repeater {
|
|
||||||
// // model: root.connectionsModel
|
|
||||||
|
|
||||||
// // delegate: TaskConnection {
|
|
||||||
// // connection: connectionData
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Flow info tooltip
|
|
||||||
// Rectangle {
|
|
||||||
// id: infoTooltip
|
|
||||||
// anchors.top: parent.bottom
|
|
||||||
// anchors.left: parent.left
|
|
||||||
// anchors.topMargin: 5
|
|
||||||
// width: infoText.width + 20
|
|
||||||
// height: infoText.height + 10
|
|
||||||
// color: palette.base
|
|
||||||
// border.color: palette.shadow
|
|
||||||
// border.width: 1
|
|
||||||
// radius: 4
|
|
||||||
// visible: false
|
|
||||||
|
|
||||||
// Text {
|
|
||||||
// id: infoText
|
|
||||||
// anchors.centerIn: parent
|
|
||||||
// text: "Tasks: " + (root.taskModel ? root.taskModel.rowCount() : 0)
|
|
||||||
// color: palette.text
|
|
||||||
// font.pixelSize: 10
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// MouseArea {
|
|
||||||
// anchors.fill: parent
|
|
||||||
// hoverEnabled: true
|
|
||||||
|
|
||||||
// onEntered: infoTooltip.visible = true
|
|
||||||
// onExited: infoTooltip.visible = false
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import TaskFlow.Editor
|
|
||||||
|
|
||||||
FlowEditor {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: 1200
|
|
||||||
height: 800
|
|
||||||
|
|
||||||
property SystemPalette sysPalette: SystemPalette {
|
|
||||||
colorGroup: SystemPalette.Active
|
|
||||||
}
|
|
||||||
palette {
|
|
||||||
window: sysPalette.window
|
|
||||||
windowText: sysPalette.windowText
|
|
||||||
base: sysPalette.base
|
|
||||||
alternateBase: sysPalette.alternateBase
|
|
||||||
text: sysPalette.text
|
|
||||||
button: sysPalette.button
|
|
||||||
buttonText: sysPalette.buttonText
|
|
||||||
highlight: sysPalette.highlight
|
|
||||||
highlightedText: sysPalette.highlightedText
|
|
||||||
light: sysPalette.light
|
|
||||||
mid: sysPalette.mid
|
|
||||||
dark: sysPalette.dark
|
|
||||||
shadow: sysPalette.shadow
|
|
||||||
brightText: sysPalette.brightText
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background with grid pattern
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: palette.window
|
|
||||||
|
|
||||||
// Grid pattern using C++ implementation
|
|
||||||
GridBackground {
|
|
||||||
anchors.fill: parent
|
|
||||||
gridSize: 20
|
|
||||||
gridColor: palette.mid
|
|
||||||
opacity: 0.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Header panel
|
|
||||||
Rectangle {
|
|
||||||
id: headerPanel
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
height: 60
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: 20
|
|
||||||
spacing: 20
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Flow Editor"
|
|
||||||
color: palette.windowText
|
|
||||||
font.pixelSize: 18
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 2
|
|
||||||
height: 30
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Flow:"
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 14
|
|
||||||
}
|
|
||||||
|
|
||||||
ComboBox {
|
|
||||||
id: flowComboBox
|
|
||||||
|
|
||||||
model: root.flowsModel
|
|
||||||
textRole: "flowId"
|
|
||||||
currentIndex: root.currentFlowIndex
|
|
||||||
|
|
||||||
onActivated: {
|
|
||||||
root.currentFlowIndex = currentIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Available Tasks: " + root.availableTaskTypes.join(", ")
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main flow area
|
|
||||||
ScrollView {
|
|
||||||
id: scrollView
|
|
||||||
anchors.top: headerPanel.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
|
|
||||||
contentWidth: flow.width
|
|
||||||
contentHeight: flow.height
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
id: flow
|
|
||||||
|
|
||||||
// flow: root.currentFlow
|
|
||||||
|
|
||||||
width: Math.max(root.width, 0)
|
|
||||||
height: Math.min(root.height, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import TaskFlow.Editor
|
|
||||||
|
|
||||||
TaskItem{
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: 280
|
|
||||||
height: Math.max(200, contentColumn.height + 40)
|
|
||||||
|
|
||||||
DragHandler {
|
|
||||||
id: dragHandler
|
|
||||||
|
|
||||||
target: root
|
|
||||||
onActiveChanged: {
|
|
||||||
if (active) {
|
|
||||||
root.z = 1000; // Поднять над остальными
|
|
||||||
} else {
|
|
||||||
root.z = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task node background
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: palette.window
|
|
||||||
border.color: palette.shadow
|
|
||||||
border.width: 1
|
|
||||||
radius: 6
|
|
||||||
|
|
||||||
// Task header
|
|
||||||
Rectangle {
|
|
||||||
id: taskHeader
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
height: 40
|
|
||||||
color: palette.button
|
|
||||||
radius: 6
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
height: parent.radius
|
|
||||||
color: parent.color
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
// text: root.taskType
|
|
||||||
color: palette.buttonText
|
|
||||||
font.pixelSize: 14
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task content
|
|
||||||
Column {
|
|
||||||
id: contentColumn
|
|
||||||
anchors.top: taskHeader.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 10
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
// Task ID
|
|
||||||
Text {
|
|
||||||
text: "ID: " + root.taskId
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 11
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameters section
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: paramColumn.height
|
|
||||||
// visible: root.parameters && root.parameters.rowCount() > 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: paramColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Parameters:"
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.parameters
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 24
|
|
||||||
color: palette.base
|
|
||||||
radius: 4
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: 8
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: paramKey + ":"
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 9
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: paramValue
|
|
||||||
color: palette.windowText
|
|
||||||
font.pixelSize: 9
|
|
||||||
width: Math.min(150, implicitWidth)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input ports section (left side)
|
|
||||||
Column {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: -8
|
|
||||||
spacing: 6
|
|
||||||
// visible: root.inputPorts && root.inputPorts.rowCount() > 0
|
|
||||||
|
|
||||||
// Input label
|
|
||||||
Text {
|
|
||||||
text: "IN"
|
|
||||||
color: palette.highlight
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: -20
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repeater {
|
|
||||||
// model: root.inputPorts
|
|
||||||
// delegate: Row {
|
|
||||||
// spacing: 6
|
|
||||||
|
|
||||||
// Text {
|
|
||||||
// text: taskPortName
|
|
||||||
// color: palette.text
|
|
||||||
// font.pixelSize: 9
|
|
||||||
// anchors.verticalCenter: parent.verticalCenter
|
|
||||||
// horizontalAlignment: Text.AlignRight
|
|
||||||
// width: 60
|
|
||||||
// elide: Text.ElideLeft
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TaskPort {
|
|
||||||
// port: taskPortData
|
|
||||||
// isInput: true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output ports section (right side)
|
|
||||||
Column {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.rightMargin: -10
|
|
||||||
spacing: 8
|
|
||||||
// visible: root.outputPorts && root.outputPorts.rowCount() > 0
|
|
||||||
|
|
||||||
// Output label
|
|
||||||
Text {
|
|
||||||
text: "OUT"
|
|
||||||
color: palette.highlight
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: -24
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repeater {
|
|
||||||
// model: root.outputPorts
|
|
||||||
// delegate: Row {
|
|
||||||
// spacing: 6
|
|
||||||
|
|
||||||
// TaskPort {
|
|
||||||
// port: taskPortData
|
|
||||||
// isInput: false
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Text {
|
|
||||||
// text: taskPortName
|
|
||||||
// color: palette.text
|
|
||||||
// font.pixelSize: 9
|
|
||||||
// anchors.verticalCenter: parent.verticalCenter
|
|
||||||
// width: 60
|
|
||||||
// elide: Text.ElideRight
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import TaskFlow.Editor
|
|
||||||
|
|
||||||
TaskConnectionItem {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property color connectionColor: "red"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 10
|
|
||||||
height: 10
|
|
||||||
radius: width / 2
|
|
||||||
color: "blue"
|
|
||||||
}
|
|
||||||
|
|
||||||
// width: Math.abs(endPoint.x - startPoint.x) + 40
|
|
||||||
// height: Math.abs(endPoint.y - startPoint.y) + 40
|
|
||||||
// x: Math.min(startPoint.x, endPoint.x) - 20
|
|
||||||
// y: Math.min(startPoint.y, endPoint.y) - 20
|
|
||||||
|
|
||||||
// Shape {
|
|
||||||
// anchors.fill: parent
|
|
||||||
|
|
||||||
// ShapePath {
|
|
||||||
// strokeWidth: 2
|
|
||||||
// strokeColor: connectionColor
|
|
||||||
// fillColor: "transparent"
|
|
||||||
|
|
||||||
// property point localStart: Qt.point(
|
|
||||||
// root.startPoint.x - root.x,
|
|
||||||
// root.startPoint.y - root.y
|
|
||||||
// )
|
|
||||||
// property point localEnd: Qt.point(
|
|
||||||
// root.endPoint.x - root.x,
|
|
||||||
// root.endPoint.y - root.y
|
|
||||||
// )
|
|
||||||
|
|
||||||
// // Bezier curve
|
|
||||||
// property real controlOffset: Math.max(50, Math.abs(localEnd.x - localStart.x) * 0.4)
|
|
||||||
|
|
||||||
// startX: localStart.x
|
|
||||||
// startY: localStart.y
|
|
||||||
|
|
||||||
// PathCubic {
|
|
||||||
// x: parent.localEnd.x
|
|
||||||
// y: parent.localEnd.y
|
|
||||||
// control1X: parent.localStart.x + parent.controlOffset
|
|
||||||
// control1Y: parent.localStart.y
|
|
||||||
// control2X: parent.localEnd.x - parent.controlOffset
|
|
||||||
// control2Y: parent.localEnd.y
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Arrow head
|
|
||||||
// Rectangle {
|
|
||||||
// width: 8
|
|
||||||
// height: 8
|
|
||||||
// color: connectionColor
|
|
||||||
// rotation: 45
|
|
||||||
// x: root.endPoint.x - root.x - 4
|
|
||||||
// y: root.endPoint.y - root.y - 4
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Update positions when tasks might have moved
|
|
||||||
// Component.onCompleted: updatePositions()
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import TaskFlow.Editor
|
|
||||||
|
|
||||||
Item {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import TaskFlow.Editor
|
|
||||||
|
|
||||||
TaskPortItem {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool isInput: true
|
|
||||||
|
|
||||||
width: 20
|
|
||||||
height: 20
|
|
||||||
|
|
||||||
// Port circle
|
|
||||||
Rectangle {
|
|
||||||
id: portCircle
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
radius: 8
|
|
||||||
color: getPortColor()
|
|
||||||
border.color: palette.windowText
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
// Inner circle for connected state simulation
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 8
|
|
||||||
height: 8
|
|
||||||
radius: 4
|
|
||||||
color: root.port ? palette.windowText : "transparent"
|
|
||||||
visible: root.port !== null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
onEntered: {
|
|
||||||
portCircle.scale = 1.3
|
|
||||||
portCircle.border.width = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
onExited: {
|
|
||||||
portCircle.scale = 1.0
|
|
||||||
portCircle.border.width = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPortColor() {
|
|
||||||
if (!root.port) return palette.mid
|
|
||||||
|
|
||||||
// Different colors for input/output using system palette
|
|
||||||
if (root.isInput) {
|
|
||||||
return palette.highlight // System highlight color for inputs
|
|
||||||
} else {
|
|
||||||
return Qt.lighter(palette.highlight, 1.3) // Lighter highlight for outputs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation { duration: 100 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "BaseTask.hpp"
|
|
||||||
#include "TaskPort.hpp"
|
|
||||||
#include <QUuid>
|
|
||||||
#include <QtConcurrent>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
BaseTask::BaseTask(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_taskId("unknown" + QUuid::createUuid().toString())
|
|
||||||
{}
|
|
||||||
|
|
||||||
BaseTask::~BaseTask()
|
|
||||||
{
|
|
||||||
qDeleteAll(m_inputs);
|
|
||||||
qDeleteAll(m_outputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BaseTask::taskId() const
|
|
||||||
{
|
|
||||||
return m_taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BaseTask::setTaskId(const QString &taskId)
|
|
||||||
{
|
|
||||||
m_taskId = taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BaseTask::taskType() const
|
|
||||||
{
|
|
||||||
return QString(metaObject()->className()).split("::").last();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BaseTask::addInputPort(const QString &name)
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_tasksMutex);
|
|
||||||
m_inputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
void BaseTask::addOutputPort(const QString &name)
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_tasksMutex);
|
|
||||||
m_outputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPort *BaseTask::inputPort(const QString &name) const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_tasksMutex);
|
|
||||||
|
|
||||||
auto it = std::find_if(m_inputs.begin(), m_inputs.end(), [&name](const TaskPort *port) {
|
|
||||||
return port->name() == name;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (it != m_inputs.end()) ? *it : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPort *BaseTask::outputPort(const QString &name) const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_tasksMutex);
|
|
||||||
|
|
||||||
auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&name](const TaskPort *port) {
|
|
||||||
return port->name() == name;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (it != m_outputs.end()) ? *it : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<TaskPort *> BaseTask::getInputPorts() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_tasksMutex);
|
|
||||||
return m_inputs;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<TaskPort *> BaseTask::getOutputPorts() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_tasksMutex);
|
|
||||||
return m_outputs;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<TaskState> BaseTask::executeAsync()
|
|
||||||
{
|
|
||||||
return QtConcurrent::task([this]() -> TaskState { return execute(); }).spawn();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString BaseTask::taskStateAsString(TaskState state)
|
|
||||||
{
|
|
||||||
switch (state) {
|
|
||||||
case TaskState::Success:
|
|
||||||
return "Success";
|
|
||||||
case TaskState::Failed:
|
|
||||||
return "Failed";
|
|
||||||
case TaskState::Cancelled:
|
|
||||||
return "Cancelled";
|
|
||||||
}
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QFuture>
|
|
||||||
#include <QMetaType>
|
|
||||||
#include <QMutex>
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskPort;
|
|
||||||
|
|
||||||
enum class TaskState { Success, Failed, Cancelled };
|
|
||||||
|
|
||||||
class BaseTask : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit BaseTask(QObject *parent = nullptr);
|
|
||||||
virtual ~BaseTask();
|
|
||||||
|
|
||||||
QString taskId() const;
|
|
||||||
void setTaskId(const QString &taskId);
|
|
||||||
QString taskType() const;
|
|
||||||
|
|
||||||
void addInputPort(const QString &name);
|
|
||||||
void addOutputPort(const QString &name);
|
|
||||||
|
|
||||||
TaskPort *inputPort(const QString &name) const;
|
|
||||||
TaskPort *outputPort(const QString &name) const;
|
|
||||||
|
|
||||||
QList<TaskPort *> getInputPorts() const;
|
|
||||||
QList<TaskPort *> getOutputPorts() const;
|
|
||||||
|
|
||||||
virtual TaskState execute() = 0;
|
|
||||||
|
|
||||||
static QString taskStateAsString(TaskState state);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
QFuture<TaskState> executeAsync();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_taskId;
|
|
||||||
QList<TaskPort *> m_inputs;
|
|
||||||
QList<TaskPort *> m_outputs;
|
|
||||||
mutable QMutex m_tasksMutex;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
qt_add_library(TaskFlowCore STATIC
|
|
||||||
BaseTask.hpp BaseTask.cpp
|
|
||||||
TaskConnection.hpp TaskConnection.cpp
|
|
||||||
Flow.hpp Flow.cpp
|
|
||||||
TaskPort.hpp TaskPort.cpp
|
|
||||||
TaskRegistry.hpp TaskRegistry.cpp
|
|
||||||
FlowManager.hpp FlowManager.cpp
|
|
||||||
FlowRegistry.hpp FlowRegistry.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(TaskFlowCore
|
|
||||||
PUBLIC
|
|
||||||
Qt::Core
|
|
||||||
Qt::Concurrent
|
|
||||||
PRIVATE
|
|
||||||
QodeAssistLogger
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(TaskFlowCore
|
|
||||||
PUBLIC
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
|
||||||
)
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "Flow.hpp"
|
|
||||||
#include "TaskPort.hpp"
|
|
||||||
#include <QUuid>
|
|
||||||
#include <QtConcurrent>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
Flow::Flow(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_flowId("flow_" + QUuid::createUuid().toString())
|
|
||||||
{}
|
|
||||||
|
|
||||||
Flow::~Flow()
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
qDeleteAll(m_connections);
|
|
||||||
qDeleteAll(m_tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Flow::flowId() const
|
|
||||||
{
|
|
||||||
return m_flowId;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Flow::setFlowId(const QString &flowId)
|
|
||||||
{
|
|
||||||
if (m_flowId != flowId) {
|
|
||||||
m_flowId = flowId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Flow::addTask(BaseTask *task)
|
|
||||||
{
|
|
||||||
if (!task) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
|
|
||||||
QString taskId = task->taskId();
|
|
||||||
if (m_tasks.contains(taskId)) {
|
|
||||||
qWarning() << "Flow::addTask - Task with ID" << taskId << "already exists";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_tasks.insert(taskId, task);
|
|
||||||
task->setParent(this);
|
|
||||||
|
|
||||||
emit taskAdded(taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Flow::removeTask(const QString &taskId)
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
|
|
||||||
BaseTask *task = m_tasks.value(taskId);
|
|
||||||
if (!task) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto it = m_connections.begin();
|
|
||||||
while (it != m_connections.end()) {
|
|
||||||
TaskConnection *connection = *it;
|
|
||||||
if (connection->sourceTask() == task || connection->targetTask() == task) {
|
|
||||||
it = m_connections.erase(it);
|
|
||||||
emit connectionRemoved(connection);
|
|
||||||
delete connection;
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_tasks.remove(taskId);
|
|
||||||
emit taskRemoved(taskId);
|
|
||||||
delete task;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Flow::removeTask(BaseTask *task)
|
|
||||||
{
|
|
||||||
if (!task) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
removeTask(task->taskId());
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseTask *Flow::getTask(const QString &taskId) const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
return m_tasks.value(taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Flow::hasTask(const QString &taskId) const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
return m_tasks.contains(taskId);
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<QString, BaseTask *> Flow::tasks() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
return m_tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskConnection *Flow::addConnection(TaskPort *sourcePort, TaskPort *targetPort)
|
|
||||||
{
|
|
||||||
if (!sourcePort || !targetPort) {
|
|
||||||
qWarning() << "Flow::addConnection - Invalid ports";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify ports belong to tasks in this flow
|
|
||||||
BaseTask *sourceTask = qobject_cast<BaseTask *>(sourcePort->parent());
|
|
||||||
BaseTask *targetTask = qobject_cast<BaseTask *>(targetPort->parent());
|
|
||||||
|
|
||||||
if (!sourceTask || !targetTask) {
|
|
||||||
qWarning() << "Flow::addConnection - Ports don't belong to valid tasks";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
|
|
||||||
if (!m_tasks.contains(sourceTask->taskId()) || !m_tasks.contains(targetTask->taskId())) {
|
|
||||||
qWarning() << "Flow::addConnection - Tasks not in this flow";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (TaskConnection *existingConnection : m_connections) {
|
|
||||||
if (existingConnection->sourcePort() == sourcePort
|
|
||||||
&& existingConnection->targetPort() == targetPort) {
|
|
||||||
qWarning() << "Flow::addConnection - Connection already exists";
|
|
||||||
return existingConnection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskConnection *connection = new TaskConnection(sourcePort, targetPort, this);
|
|
||||||
m_connections.append(connection);
|
|
||||||
|
|
||||||
emit connectionAdded(connection);
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Flow::removeConnection(TaskConnection *connection)
|
|
||||||
{
|
|
||||||
if (!connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
|
|
||||||
if (m_connections.removeOne(connection)) {
|
|
||||||
emit connectionRemoved(connection);
|
|
||||||
delete connection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<TaskConnection *> Flow::connections() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
return m_connections;
|
|
||||||
}
|
|
||||||
|
|
||||||
QFuture<FlowState> Flow::executeAsync()
|
|
||||||
{
|
|
||||||
return QtConcurrent::run([this]() { return execute(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowState Flow::execute()
|
|
||||||
{
|
|
||||||
emit executionStarted();
|
|
||||||
|
|
||||||
if (!isValid()) {
|
|
||||||
emit executionFinished(FlowState::Failed);
|
|
||||||
return FlowState::Failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCircularDependencies()) {
|
|
||||||
qWarning() << "Flow::execute - Circular dependencies detected";
|
|
||||||
emit executionFinished(FlowState::Failed);
|
|
||||||
return FlowState::Failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<BaseTask *> executionOrder = getExecutionOrder();
|
|
||||||
|
|
||||||
for (BaseTask *task : executionOrder) {
|
|
||||||
TaskState taskResult = task->execute();
|
|
||||||
|
|
||||||
if (taskResult == TaskState::Failed) {
|
|
||||||
qWarning() << "Flow::execute - Task" << task->taskId() << "failed";
|
|
||||||
emit executionFinished(FlowState::Failed);
|
|
||||||
return FlowState::Failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskResult == TaskState::Cancelled) {
|
|
||||||
qWarning() << "Flow::execute - Task" << task->taskId() << "cancelled";
|
|
||||||
emit executionFinished(FlowState::Cancelled);
|
|
||||||
return FlowState::Cancelled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit executionFinished(FlowState::Success);
|
|
||||||
return FlowState::Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Flow::isValid() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
|
|
||||||
// Check all connections are valid
|
|
||||||
for (TaskConnection *connection : m_connections) {
|
|
||||||
if (!connection->isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Flow::hasCircularDependencies() const
|
|
||||||
{
|
|
||||||
return detectCircularDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Flow::flowStateAsString(FlowState state)
|
|
||||||
{
|
|
||||||
switch (state) {
|
|
||||||
case FlowState::Success:
|
|
||||||
return "Success";
|
|
||||||
case FlowState::Failed:
|
|
||||||
return "Failed";
|
|
||||||
case FlowState::Cancelled:
|
|
||||||
return "Cancelled";
|
|
||||||
}
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList Flow::getTaskIds() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
return m_tasks.keys();
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<BaseTask *> Flow::getExecutionOrder() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
|
|
||||||
QList<BaseTask *> result;
|
|
||||||
QSet<BaseTask *> visited;
|
|
||||||
QList<BaseTask *> allTasks = m_tasks.values();
|
|
||||||
|
|
||||||
std::function<void(BaseTask *)> visit = [&](BaseTask *task) {
|
|
||||||
if (visited.contains(task)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.insert(task);
|
|
||||||
|
|
||||||
QList<BaseTask *> dependencies = getTaskDependencies(task);
|
|
||||||
for (BaseTask *dependency : dependencies) {
|
|
||||||
visit(dependency);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append(task);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (BaseTask *task : allTasks) {
|
|
||||||
visit(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Flow::detectCircularDependencies() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_flowMutex);
|
|
||||||
|
|
||||||
QSet<BaseTask *> visited;
|
|
||||||
QSet<BaseTask *> recursionStack;
|
|
||||||
bool hasCycle = false;
|
|
||||||
|
|
||||||
for (BaseTask *task : m_tasks.values()) {
|
|
||||||
if (!visited.contains(task)) {
|
|
||||||
visitTask(task, visited, recursionStack, hasCycle);
|
|
||||||
if (hasCycle) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Flow::visitTask(
|
|
||||||
BaseTask *task, QSet<BaseTask *> &visited, QSet<BaseTask *> &recursionStack, bool &hasCycle) const
|
|
||||||
{
|
|
||||||
if (hasCycle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.insert(task);
|
|
||||||
recursionStack.insert(task);
|
|
||||||
|
|
||||||
for (TaskConnection *connection : m_connections) {
|
|
||||||
if (connection->sourceTask() == task) {
|
|
||||||
BaseTask *dependentTask = connection->targetTask();
|
|
||||||
|
|
||||||
if (recursionStack.contains(dependentTask)) {
|
|
||||||
hasCycle = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!visited.contains(dependentTask)) {
|
|
||||||
visitTask(dependentTask, visited, recursionStack, hasCycle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recursionStack.remove(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<BaseTask *> Flow::getTaskDependencies(BaseTask *task) const
|
|
||||||
{
|
|
||||||
QList<BaseTask *> dependencies;
|
|
||||||
|
|
||||||
for (TaskConnection *connection : m_connections) {
|
|
||||||
if (connection->targetTask() == task) {
|
|
||||||
BaseTask *dependencyTask = connection->sourceTask();
|
|
||||||
if (!dependencies.contains(dependencyTask)) {
|
|
||||||
dependencies.append(dependencyTask);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QFuture>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QList>
|
|
||||||
#include <QMetaType>
|
|
||||||
#include <QMutex>
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include "BaseTask.hpp"
|
|
||||||
#include "TaskConnection.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
enum class FlowState { Success, Failed, Cancelled };
|
|
||||||
|
|
||||||
class Flow : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit Flow(QObject *parent = nullptr);
|
|
||||||
~Flow() override;
|
|
||||||
|
|
||||||
QString flowId() const;
|
|
||||||
void setFlowId(const QString &flowId);
|
|
||||||
|
|
||||||
void addTask(BaseTask *task);
|
|
||||||
void removeTask(const QString &taskId);
|
|
||||||
void removeTask(BaseTask *task);
|
|
||||||
|
|
||||||
BaseTask *getTask(const QString &taskId) const;
|
|
||||||
bool hasTask(const QString &taskId) const;
|
|
||||||
QHash<QString, BaseTask *> tasks() const;
|
|
||||||
|
|
||||||
TaskConnection *addConnection(TaskPort *sourcePort, TaskPort *targetPort);
|
|
||||||
void removeConnection(TaskConnection *connection);
|
|
||||||
QList<TaskConnection *> connections() const;
|
|
||||||
|
|
||||||
QFuture<FlowState> executeAsync();
|
|
||||||
virtual FlowState execute();
|
|
||||||
|
|
||||||
bool isValid() const;
|
|
||||||
bool hasCircularDependencies() const;
|
|
||||||
|
|
||||||
static QString flowStateAsString(FlowState state);
|
|
||||||
QStringList getTaskIds() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void taskAdded(const QString &taskId);
|
|
||||||
void taskRemoved(const QString &taskId);
|
|
||||||
void connectionAdded(QodeAssist::TaskFlow::TaskConnection *connection);
|
|
||||||
void connectionRemoved(QodeAssist::TaskFlow::TaskConnection *connection);
|
|
||||||
void executionStarted();
|
|
||||||
void executionFinished(FlowState result);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_flowId;
|
|
||||||
QHash<QString, BaseTask *> m_tasks;
|
|
||||||
QList<TaskConnection *> m_connections;
|
|
||||||
mutable QMutex m_flowMutex;
|
|
||||||
|
|
||||||
QList<BaseTask *> getExecutionOrder() const;
|
|
||||||
bool detectCircularDependencies() const;
|
|
||||||
void visitTask(
|
|
||||||
BaseTask *task,
|
|
||||||
QSet<BaseTask *> &visited,
|
|
||||||
QSet<BaseTask *> &recursionStack,
|
|
||||||
bool &hasCycle) const;
|
|
||||||
QList<BaseTask *> getTaskDependencies(BaseTask *task) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::Flow *)
|
|
||||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::FlowState)
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowManager.hpp"
|
|
||||||
|
|
||||||
#include <Logger.hpp>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
|
|
||||||
#include "FlowRegistry.hpp"
|
|
||||||
#include "TaskRegistry.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
FlowManager::FlowManager(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_taskRegistry(new TaskRegistry(this))
|
|
||||||
, m_flowRegistry(new FlowRegistry(this))
|
|
||||||
{
|
|
||||||
LOG_MESSAGE("FlowManager created");
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowManager::~FlowManager()
|
|
||||||
{
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flow *FlowManager::createFlow(const QString &flowId)
|
|
||||||
// {
|
|
||||||
// Flow *flow = new Flow(flowId, m_taskRegistry, this);
|
|
||||||
// if (!m_flows.contains(flow->flowId())) {
|
|
||||||
// m_flows.insert(flowId, flow);
|
|
||||||
// } else {
|
|
||||||
// LOG_MESSAGE(
|
|
||||||
// QString("FlowManager::createFlow - flow with id %1 already exists").arg(flow->flowId()));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return flow;
|
|
||||||
// }
|
|
||||||
|
|
||||||
void FlowManager::addFlow(Flow *flow)
|
|
||||||
{
|
|
||||||
qDebug() << "FlowManager::addFlow" << flow->flowId();
|
|
||||||
if (!m_flows.contains(flow->flowId())) {
|
|
||||||
m_flows.insert(flow->flowId(), flow);
|
|
||||||
flow->setParent(this);
|
|
||||||
emit flowAdded(flow->flowId());
|
|
||||||
} else {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("FlowManager::addFlow - flow with id %1 already exists").arg(flow->flowId()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FlowManager::clear()
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("FlowManager::clear - removing %1 flows").arg(m_flows.size()));
|
|
||||||
|
|
||||||
qDeleteAll(m_flows);
|
|
||||||
m_flows.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList FlowManager::getAvailableTasksTypes()
|
|
||||||
{
|
|
||||||
return m_taskRegistry->getAvailableTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList FlowManager::getAvailableFlows()
|
|
||||||
{
|
|
||||||
return m_flowRegistry->getAvailableTypes();
|
|
||||||
}
|
|
||||||
|
|
||||||
QHash<QString, Flow *> FlowManager::flows() const
|
|
||||||
{
|
|
||||||
return m_flows;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskRegistry *FlowManager::taskRegistry() const
|
|
||||||
{
|
|
||||||
return m_taskRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
FlowRegistry *FlowManager::flowRegistry() const
|
|
||||||
{
|
|
||||||
return m_flowRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow *FlowManager::getFlow(const QString &flowId) const
|
|
||||||
{
|
|
||||||
// if (flowId.isEmpty()) {
|
|
||||||
// return m_flows.begin().value();
|
|
||||||
// }
|
|
||||||
// return m_flows.value(flowId, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QHash>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
#include "Flow.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskRegistry;
|
|
||||||
class FlowRegistry;
|
|
||||||
|
|
||||||
class FlowManager : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit FlowManager(QObject *parent = nullptr);
|
|
||||||
~FlowManager() override;
|
|
||||||
|
|
||||||
// Flow *createFlow(const QString &flowId);
|
|
||||||
void addFlow(Flow *flow);
|
|
||||||
|
|
||||||
void clear();
|
|
||||||
|
|
||||||
QStringList getAvailableTasksTypes();
|
|
||||||
QStringList getAvailableFlows();
|
|
||||||
|
|
||||||
QHash<QString, Flow *> flows() const;
|
|
||||||
|
|
||||||
TaskRegistry *taskRegistry() const;
|
|
||||||
FlowRegistry *flowRegistry() const;
|
|
||||||
|
|
||||||
Flow *getFlow(const QString &flowId = {}) const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void flowAdded(const QString &flowId);
|
|
||||||
void flowRemoved(const QString &flowId);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QHash<QString, Flow *> m_flows;
|
|
||||||
|
|
||||||
TaskRegistry *m_taskRegistry;
|
|
||||||
FlowRegistry *m_flowRegistry;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowRegistry.hpp"
|
|
||||||
#include "Logger.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
FlowRegistry::FlowRegistry(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{}
|
|
||||||
|
|
||||||
void FlowRegistry::registerFlow(const QString &flowType, FlowCreator creator)
|
|
||||||
{
|
|
||||||
m_flowCreators[flowType] = creator;
|
|
||||||
LOG_MESSAGE(QString("FlowRegistry: Registered flow type '%1'").arg(flowType));
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow *FlowRegistry::createFlow(const QString &flowType, FlowManager *flowManager) const
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Trying to create flow: %1").arg(flowType));
|
|
||||||
|
|
||||||
if (m_flowCreators.contains(flowType)) {
|
|
||||||
LOG_MESSAGE(QString("Found creator for flow type: %1").arg(flowType));
|
|
||||||
try {
|
|
||||||
Flow *flow = m_flowCreators[flowType](flowManager);
|
|
||||||
if (flow) {
|
|
||||||
LOG_MESSAGE(QString("Successfully created flow: %1").arg(flowType));
|
|
||||||
return flow;
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
LOG_MESSAGE(QString("Exception while creating flow of type: %1").arg(flowType));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_MESSAGE(QString("No creator found for flow type: %1").arg(flowType));
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList FlowRegistry::getAvailableTypes() const
|
|
||||||
{
|
|
||||||
return m_flowCreators.keys();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class Flow;
|
|
||||||
class FlowManager;
|
|
||||||
|
|
||||||
class FlowRegistry : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
using FlowCreator = std::function<Flow *(FlowManager *flowManager)>;
|
|
||||||
|
|
||||||
explicit FlowRegistry(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
void registerFlow(const QString &flowType, FlowCreator creator);
|
|
||||||
Flow *createFlow(const QString &flowType, FlowManager *flowManager = nullptr) const;
|
|
||||||
QStringList getAvailableTypes() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QHash<QString, FlowCreator> m_flowCreators;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskConnection.hpp"
|
|
||||||
#include "BaseTask.hpp"
|
|
||||||
#include "TaskPort.hpp"
|
|
||||||
#include <QMetaEnum>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskConnection::TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_sourcePort(sourcePort)
|
|
||||||
, m_targetPort(targetPort)
|
|
||||||
{
|
|
||||||
setupConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskConnection::~TaskConnection()
|
|
||||||
{
|
|
||||||
cleanupConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseTask *TaskConnection::sourceTask() const
|
|
||||||
{
|
|
||||||
return m_sourcePort ? qobject_cast<BaseTask *>(m_sourcePort->parent()) : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseTask *TaskConnection::targetTask() const
|
|
||||||
{
|
|
||||||
return m_targetPort ? qobject_cast<BaseTask *>(m_targetPort->parent()) : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPort *TaskConnection::sourcePort() const
|
|
||||||
{
|
|
||||||
return m_sourcePort;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPort *TaskConnection::targetPort() const
|
|
||||||
{
|
|
||||||
return m_targetPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TaskConnection::isValid() const
|
|
||||||
{
|
|
||||||
return m_sourcePort && m_targetPort && m_sourcePort != m_targetPort && sourceTask()
|
|
||||||
&& targetTask() && sourceTask() != targetTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TaskConnection::isTypeCompatible() const
|
|
||||||
{
|
|
||||||
if (!isValid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_targetPort->isConnectionTypeCompatible(m_sourcePort);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TaskConnection::toString() const
|
|
||||||
{
|
|
||||||
if (!isValid()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseTask *srcTask = sourceTask();
|
|
||||||
BaseTask *tgtTask = targetTask();
|
|
||||||
|
|
||||||
return QString("%1.%2->%3.%4")
|
|
||||||
.arg(srcTask->taskId())
|
|
||||||
.arg(m_sourcePort->name())
|
|
||||||
.arg(tgtTask->taskId())
|
|
||||||
.arg(m_targetPort->name());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TaskConnection::operator==(const TaskConnection &other) const
|
|
||||||
{
|
|
||||||
return m_sourcePort == other.m_sourcePort && m_targetPort == other.m_targetPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskConnection::setupConnection()
|
|
||||||
{
|
|
||||||
if (!isValid()) {
|
|
||||||
qWarning() << "TaskConnection::setupConnection - Invalid connection parameters";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTypeCompatible()) {
|
|
||||||
QMetaEnum metaEnum = QMetaEnum::fromType<TaskPort::ValueType>();
|
|
||||||
qWarning() << "TaskConnection::setupConnection - Type incompatible connection:"
|
|
||||||
<< metaEnum.valueToKey(static_cast<int>(m_sourcePort->valueType())) << "to"
|
|
||||||
<< metaEnum.valueToKey(static_cast<int>(m_targetPort->valueType()));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_sourcePort->setConnection(this);
|
|
||||||
m_targetPort->setConnection(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskConnection::cleanupConnection()
|
|
||||||
{
|
|
||||||
if (m_sourcePort && m_sourcePort->connection() == this) {
|
|
||||||
m_sourcePort->setConnection(nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_targetPort && m_targetPort->connection() == this) {
|
|
||||||
m_targetPort->setConnection(nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class BaseTask;
|
|
||||||
class TaskPort;
|
|
||||||
|
|
||||||
class TaskConnection : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor automatically sets up the connection
|
|
||||||
explicit TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
// Destructor automatically cleans up the connection
|
|
||||||
~TaskConnection() override;
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
BaseTask *sourceTask() const;
|
|
||||||
BaseTask *targetTask() const;
|
|
||||||
TaskPort *sourcePort() const;
|
|
||||||
TaskPort *targetPort() const;
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
bool isValid() const;
|
|
||||||
bool isTypeCompatible() const;
|
|
||||||
|
|
||||||
// Utility
|
|
||||||
QString toString() const;
|
|
||||||
|
|
||||||
// Comparison
|
|
||||||
bool operator==(const TaskConnection &other) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
TaskPort *m_sourcePort;
|
|
||||||
TaskPort *m_targetPort;
|
|
||||||
|
|
||||||
void setupConnection();
|
|
||||||
void cleanupConnection();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskPort.hpp"
|
|
||||||
#include "TaskConnection.hpp"
|
|
||||||
#include <QMetaEnum>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskPort::TaskPort(const QString &name, ValueType type, QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_name(name)
|
|
||||||
, m_valueType(type)
|
|
||||||
{}
|
|
||||||
|
|
||||||
QString TaskPort::name() const
|
|
||||||
{
|
|
||||||
return m_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskPort::setValueType(ValueType type)
|
|
||||||
{
|
|
||||||
if (m_valueType != type)
|
|
||||||
m_valueType = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskPort::ValueType TaskPort::valueType() const
|
|
||||||
{
|
|
||||||
return m_valueType;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskPort::setValue(const QVariant &value)
|
|
||||||
{
|
|
||||||
if (!isValueTypeCompatible(value)) {
|
|
||||||
qWarning() << "TaskPort::setValue - Type mismatch for port" << m_name << "Expected:"
|
|
||||||
<< QMetaEnum::fromType<ValueType>().valueToKey(static_cast<int>(m_valueType))
|
|
||||||
<< "Got:" << value.typeName();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_value != value) {
|
|
||||||
m_value = value;
|
|
||||||
emit valueChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant TaskPort::value() const
|
|
||||||
{
|
|
||||||
if (hasConnection() && m_connection->sourcePort()) {
|
|
||||||
return m_connection->sourcePort()->m_value;
|
|
||||||
}
|
|
||||||
return m_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaskPort::setConnection(TaskConnection *connection)
|
|
||||||
{
|
|
||||||
if (m_connection != connection) {
|
|
||||||
m_connection = connection;
|
|
||||||
emit connectionChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskConnection *TaskPort::connection() const
|
|
||||||
{
|
|
||||||
return m_connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TaskPort::hasConnection() const
|
|
||||||
{
|
|
||||||
return m_connection != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TaskPort::isValueTypeCompatible(const QVariant &value) const
|
|
||||||
{
|
|
||||||
if (m_valueType == ValueType::Any) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (m_valueType) {
|
|
||||||
case ValueType::String:
|
|
||||||
return value.canConvert<QString>();
|
|
||||||
|
|
||||||
case ValueType::Number:
|
|
||||||
return value.canConvert<double>() || value.canConvert<int>();
|
|
||||||
|
|
||||||
case ValueType::Boolean:
|
|
||||||
return value.canConvert<bool>();
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TaskPort::isConnectionTypeCompatible(const TaskPort *sourcePort) const
|
|
||||||
{
|
|
||||||
if (!sourcePort) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourcePort->valueType() == ValueType::Any || m_valueType == ValueType::Any) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sourcePort->valueType() == m_valueType;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
#include <QVariant>
|
|
||||||
|
|
||||||
#include "TaskConnection.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class TaskPort : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum class ValueType {
|
|
||||||
Any, // QVariant
|
|
||||||
String, // QString
|
|
||||||
Number, // int/double
|
|
||||||
Boolean // bool
|
|
||||||
};
|
|
||||||
Q_ENUM(ValueType)
|
|
||||||
|
|
||||||
explicit TaskPort(
|
|
||||||
const QString &name, ValueType type = ValueType::Any, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QString name() const;
|
|
||||||
|
|
||||||
ValueType valueType() const;
|
|
||||||
void setValueType(ValueType type);
|
|
||||||
|
|
||||||
void setValue(const QVariant &value);
|
|
||||||
QVariant value() const;
|
|
||||||
|
|
||||||
void setConnection(TaskConnection *connection);
|
|
||||||
TaskConnection *connection() const;
|
|
||||||
bool hasConnection() const;
|
|
||||||
|
|
||||||
bool isValueTypeCompatible(const QVariant &value) const;
|
|
||||||
bool isConnectionTypeCompatible(const TaskPort *sourcePort) const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void valueChanged();
|
|
||||||
void connectionChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString m_name;
|
|
||||||
ValueType m_valueType;
|
|
||||||
QVariant m_value;
|
|
||||||
TaskConnection *m_connection = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort *)
|
|
||||||
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort::ValueType)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskRegistry.hpp"
|
|
||||||
|
|
||||||
#include <Logger.hpp>
|
|
||||||
|
|
||||||
#include "BaseTask.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
TaskRegistry::TaskRegistry(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{}
|
|
||||||
|
|
||||||
BaseTask *TaskRegistry::createTask(const QString &taskType, QObject *parent) const
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Trying to create task: %1").arg(taskType));
|
|
||||||
|
|
||||||
if (m_creators.contains(taskType)) {
|
|
||||||
LOG_MESSAGE(QString("Found creator for task type: %1").arg(taskType));
|
|
||||||
try {
|
|
||||||
BaseTask *task = m_creators[taskType](parent);
|
|
||||||
if (task) {
|
|
||||||
LOG_MESSAGE(QString("Successfully created task: %1").arg(taskType));
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
LOG_MESSAGE(QString("Exception while creating task of type: %1").arg(taskType));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_MESSAGE(QString("No creator found for task type: %1").arg(taskType));
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList TaskRegistry::getAvailableTypes() const
|
|
||||||
{
|
|
||||||
return m_creators.keys();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
|
||||||
|
|
||||||
class BaseTask;
|
|
||||||
|
|
||||||
class TaskRegistry : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
using TaskCreator = std::function<BaseTask *(QObject *parent)>;
|
|
||||||
|
|
||||||
explicit TaskRegistry(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
inline void registerTask(const QString &taskType)
|
|
||||||
{
|
|
||||||
m_creators[taskType] = [](QObject *parent) -> BaseTask * { return new T(parent); };
|
|
||||||
}
|
|
||||||
BaseTask *createTask(const QString &taskType, QObject *parent = nullptr) const;
|
|
||||||
QStringList getAvailableTypes() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QHash<QString, TaskCreator> m_creators;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::TaskFlow
|
|
||||||
@@ -13,6 +13,8 @@ Button {
|
|||||||
focusPolicy: Qt.NoFocus
|
focusPolicy: Qt.NoFocus
|
||||||
padding: 4
|
padding: 4
|
||||||
|
|
||||||
|
opacity: control.enabled ? 1.0 : 0.4
|
||||||
|
|
||||||
icon.width: 16
|
icon.width: 16
|
||||||
icon.height: 16
|
icon.height: 16
|
||||||
|
|
||||||
|
|||||||
@@ -49,12 +49,6 @@ void UpdateStatusWidget::showUpdateAvailable(const QString &version)
|
|||||||
m_updateButton->setToolTip(tr("Check update information"));
|
m_updateButton->setToolTip(tr("Check update information"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void UpdateStatusWidget::hideUpdateInfo()
|
|
||||||
{
|
|
||||||
m_versionLabel->setVisible(false);
|
|
||||||
m_updateButton->setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UpdateStatusWidget::setChatButtonAction(QAction *action)
|
void UpdateStatusWidget::setChatButtonAction(QAction *action)
|
||||||
{
|
{
|
||||||
m_chatButton->setDefaultAction(action);
|
m_chatButton->setDefaultAction(action);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ public:
|
|||||||
|
|
||||||
void setDefaultAction(QAction *action);
|
void setDefaultAction(QAction *action);
|
||||||
void showUpdateAvailable(const QString &version);
|
void showUpdateAvailable(const QString &version);
|
||||||
void hideUpdateInfo();
|
|
||||||
void setChatButtonAction(QAction *action);
|
void setChatButtonAction(QAction *action);
|
||||||
void setChatButtonMenu(QMenu *menu);
|
void setChatButtonMenu(QMenu *menu);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
add_library(Context STATIC
|
add_library(Context STATIC
|
||||||
DocumentContextReader.hpp DocumentContextReader.cpp
|
DocumentContextReader.hpp DocumentContextReader.cpp
|
||||||
|
EnvBlockFormatter.hpp EnvBlockFormatter.cpp
|
||||||
ChangesManager.h ChangesManager.cpp
|
ChangesManager.h ChangesManager.cpp
|
||||||
ContextManager.hpp ContextManager.cpp
|
ContextManager.hpp ContextManager.cpp
|
||||||
|
IProjectScanner.hpp
|
||||||
|
ProjectScannerQtCreator.hpp ProjectScannerQtCreator.cpp
|
||||||
ContentFile.hpp
|
ContentFile.hpp
|
||||||
DocumentReaderQtCreator.hpp
|
DocumentReaderQtCreator.hpp
|
||||||
IDocumentReader.hpp
|
IDocumentReader.hpp
|
||||||
@@ -21,7 +24,7 @@ target_link_libraries(Context
|
|||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::ProjectExplorer
|
QtCreator::ProjectExplorer
|
||||||
PRIVATE
|
PRIVATE
|
||||||
PluginLLMCore
|
Common
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -56,16 +56,16 @@ void ChangesManager::addChange(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChangesManager::getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const
|
QString ChangesManager::getRecentChangesContext(const QString ¤tFilePath) const
|
||||||
{
|
{
|
||||||
QString context;
|
QString context;
|
||||||
for (auto it = m_documentChanges.constBegin(); it != m_documentChanges.constEnd(); ++it) {
|
for (auto it = m_documentChanges.constBegin(); it != m_documentChanges.constEnd(); ++it) {
|
||||||
if (it.key() != currentDocument) {
|
if (it.key() && it.key()->filePath().toFSPathString() == currentFilePath)
|
||||||
|
continue;
|
||||||
for (const auto &change : it.value()) {
|
for (const auto &change : it.value()) {
|
||||||
context += change.lineContent + "\n";
|
context += change.lineContent + "\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,175 +282,6 @@ ChangesManager::FileEdit ChangesManager::getFileEdit(const QString &editId) cons
|
|||||||
return m_fileEdits.value(editId);
|
return m_fileEdits.value(editId);
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ChangesManager::FileEdit> ChangesManager::getPendingEdits() const
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
|
|
||||||
QList<FileEdit> pendingEdits;
|
|
||||||
for (const auto &edit : m_fileEdits.values()) {
|
|
||||||
if (edit.status == Pending) {
|
|
||||||
pendingEdits.append(edit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pendingEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChangesManager::performFileEdit(
|
|
||||||
const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg)
|
|
||||||
{
|
|
||||||
auto setError = [errorMsg](const QString &msg) {
|
|
||||||
if (errorMsg) *errorMsg = msg;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto editors = Core::EditorManager::visibleEditors();
|
|
||||||
for (auto *editor : editors) {
|
|
||||||
if (!editor || !editor->document()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString editorPath = editor->document()->filePath().toFSPathString();
|
|
||||||
if (editorPath == filePath) {
|
|
||||||
QByteArray contentBytes = editor->document()->contents();
|
|
||||||
QString currentContent = QString::fromUtf8(contentBytes);
|
|
||||||
|
|
||||||
if (oldContent.isEmpty()) {
|
|
||||||
if (auto *textEditor
|
|
||||||
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
|
|
||||||
QTextDocument *doc = textEditor->document();
|
|
||||||
|
|
||||||
QTextCursor cursor(doc);
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.movePosition(QTextCursor::End);
|
|
||||||
cursor.insertText(newContent);
|
|
||||||
cursor.endEditBlock();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Appended to open editor: %1").arg(filePath));
|
|
||||||
setError("Applied successfully (appended to end of file)");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int matchPos = currentContent.indexOf(oldContent);
|
|
||||||
if (matchPos != -1) {
|
|
||||||
if (auto *textEditor
|
|
||||||
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
|
|
||||||
QTextDocument *doc = textEditor->document();
|
|
||||||
|
|
||||||
QTextCursor cursor(doc);
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.setPosition(matchPos);
|
|
||||||
cursor.setPosition(matchPos + oldContent.length(), QTextCursor::KeepAnchor);
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
cursor.insertText(newContent);
|
|
||||||
cursor.endEditBlock();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Updated open editor (exact match): %1").arg(filePath));
|
|
||||||
setError("Applied successfully (exact match)");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
double similarity = 0.0;
|
|
||||||
QString matchedContent = findBestMatch(currentContent, oldContent, 0.82, &similarity);
|
|
||||||
if (!matchedContent.isEmpty()) {
|
|
||||||
matchPos = currentContent.indexOf(matchedContent);
|
|
||||||
if (matchPos != -1) {
|
|
||||||
if (auto *textEditor
|
|
||||||
= qobject_cast<TextEditor::TextDocument *>(editor->document())) {
|
|
||||||
QTextDocument *doc = textEditor->document();
|
|
||||||
|
|
||||||
QTextCursor cursor(doc);
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.setPosition(matchPos);
|
|
||||||
cursor.setPosition(matchPos + matchedContent.length(), QTextCursor::KeepAnchor);
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
cursor.insertText(newContent);
|
|
||||||
cursor.endEditBlock();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Updated open editor (fuzzy match %1%%): %2")
|
|
||||||
.arg(qRound(similarity * 100)).arg(filePath));
|
|
||||||
setError(QString("Applied with fuzzy match (%1%% similarity)").arg(qRound(similarity * 100)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Old content not found in open editor (best similarity: %1%%): %2")
|
|
||||||
.arg(qRound(similarity * 100)).arg(filePath));
|
|
||||||
setError(QString("Content not found. Best match: %1%% (threshold: 82%%). "
|
|
||||||
"File may have changed.").arg(qRound(similarity * 100)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFile file(filePath);
|
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
||||||
QString msg = QString("Cannot open file: %1").arg(file.errorString());
|
|
||||||
LOG_MESSAGE(QString("Failed to open file for reading: %1 - %2").arg(filePath, file.errorString()));
|
|
||||||
setError(msg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString currentContent = QString::fromUtf8(file.readAll());
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
QString updatedContent;
|
|
||||||
|
|
||||||
if (oldContent.isEmpty()) {
|
|
||||||
updatedContent = currentContent + newContent;
|
|
||||||
LOG_MESSAGE(QString("Appending to file: %1").arg(filePath));
|
|
||||||
setError("Applied successfully (appended to end of file)");
|
|
||||||
}
|
|
||||||
else if (currentContent.contains(oldContent)) {
|
|
||||||
int matchPos = currentContent.indexOf(oldContent);
|
|
||||||
updatedContent = currentContent.left(matchPos)
|
|
||||||
+ newContent
|
|
||||||
+ currentContent.mid(matchPos + oldContent.length());
|
|
||||||
LOG_MESSAGE(QString("Using exact match for file update: %1 at position %2")
|
|
||||||
.arg(filePath).arg(matchPos));
|
|
||||||
setError("Applied successfully (exact match)");
|
|
||||||
} else {
|
|
||||||
double similarity = 0.0;
|
|
||||||
QString matchedContent = findBestMatch(currentContent, oldContent, 0.82, &similarity);
|
|
||||||
if (!matchedContent.isEmpty()) {
|
|
||||||
int matchPos = currentContent.indexOf(matchedContent);
|
|
||||||
if (matchPos == -1) {
|
|
||||||
QString msg = "Internal error: matched content not found in file";
|
|
||||||
LOG_MESSAGE(QString("Internal error: matched content disappeared: %1").arg(filePath));
|
|
||||||
setError(msg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
updatedContent = currentContent.left(matchPos)
|
|
||||||
+ newContent
|
|
||||||
+ currentContent.mid(matchPos + matchedContent.length());
|
|
||||||
LOG_MESSAGE(QString("Using fuzzy match (%1%%) for file update: %2 at position %3")
|
|
||||||
.arg(qRound(similarity * 100)).arg(filePath).arg(matchPos));
|
|
||||||
setError(QString("Applied with fuzzy match (%1%% similarity)").arg(qRound(similarity * 100)));
|
|
||||||
} else {
|
|
||||||
QString msg = QString("Content not found. Best match: %1%% (threshold: 82%%). "
|
|
||||||
"File may have changed.").arg(qRound(similarity * 100));
|
|
||||||
LOG_MESSAGE(QString("Old content not found in file (best similarity: %1%%): %2")
|
|
||||||
.arg(qRound(similarity * 100)).arg(filePath));
|
|
||||||
setError(msg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
|
|
||||||
QString msg = QString("Cannot write file: %1").arg(file.errorString());
|
|
||||||
LOG_MESSAGE(QString("Failed to open file for writing: %1 - %2").arg(filePath, file.errorString()));
|
|
||||||
setError(msg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextStream out(&file);
|
|
||||||
out << updatedContent;
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("File updated: %1").arg(filePath));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChangesManager::levenshteinDistance(const QString &s1, const QString &s2) const
|
int ChangesManager::levenshteinDistance(const QString &s1, const QString &s2) const
|
||||||
{
|
{
|
||||||
const int len1 = s1.length();
|
const int len1 = s1.length();
|
||||||
@@ -1112,138 +943,6 @@ QString ChangesManager::readFileContent(const QString &filePath) const
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChangesManager::performFileEditWithDiff(
|
|
||||||
const QString &filePath,
|
|
||||||
const DiffInfo &diffInfo,
|
|
||||||
bool reverse,
|
|
||||||
QString *errorMsg)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("=== performFileEditWithDiff: %1 (reverse: %2) ===")
|
|
||||||
.arg(filePath).arg(reverse ? "yes" : "no"));
|
|
||||||
|
|
||||||
auto setError = [errorMsg](const QString &msg) {
|
|
||||||
if (errorMsg) *errorMsg = msg;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto editors = Core::EditorManager::visibleEditors();
|
|
||||||
LOG_MESSAGE(QString(" Checking %1 visible editor(s)").arg(editors.size()));
|
|
||||||
|
|
||||||
for (auto *editor : editors) {
|
|
||||||
if (!editor || !editor->document()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString editorPath = editor->document()->filePath().toFSPathString();
|
|
||||||
if (editorPath == filePath) {
|
|
||||||
LOG_MESSAGE(QString(" Found open editor for: %1").arg(filePath));
|
|
||||||
|
|
||||||
QByteArray contentBytes = editor->document()->contents();
|
|
||||||
QString currentContent = QString::fromUtf8(contentBytes);
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString(" Current content size: %1 bytes").arg(currentContent.size()));
|
|
||||||
|
|
||||||
QString modifiedContent = currentContent;
|
|
||||||
QString diffErrorMsg;
|
|
||||||
bool diffSuccess = applyDiffToContent(modifiedContent, diffInfo, reverse, &diffErrorMsg);
|
|
||||||
|
|
||||||
if (!diffSuccess) {
|
|
||||||
LOG_MESSAGE(QString(" Failed to apply diff: %1").arg(diffErrorMsg));
|
|
||||||
setError(diffErrorMsg);
|
|
||||||
|
|
||||||
LOG_MESSAGE(" Attempting fallback to old content-based method...");
|
|
||||||
QString oldContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
|
|
||||||
QString newContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
|
|
||||||
|
|
||||||
return performFileEdit(filePath, oldContent, newContent, errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto *textEditor = qobject_cast<TextEditor::TextDocument *>(editor->document())) {
|
|
||||||
QTextDocument *doc = textEditor->document();
|
|
||||||
|
|
||||||
LOG_MESSAGE(" Applying changes to text editor document...");
|
|
||||||
|
|
||||||
if (!doc) {
|
|
||||||
LOG_MESSAGE(" Document is invalid");
|
|
||||||
setError("Document pointer is null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
QTextCursor cursor(doc);
|
|
||||||
|
|
||||||
if (cursor.isNull()) {
|
|
||||||
LOG_MESSAGE(" Cursor is invalid");
|
|
||||||
setError("Cannot create text cursor");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.select(QTextCursor::Document);
|
|
||||||
cursor.removeSelectedText();
|
|
||||||
cursor.insertText(modifiedContent);
|
|
||||||
cursor.endEditBlock();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString(" ✓ Successfully applied diff to open editor: %1").arg(filePath));
|
|
||||||
setError(diffErrorMsg);
|
|
||||||
return true;
|
|
||||||
} catch (...) {
|
|
||||||
LOG_MESSAGE(" Exception during document modification");
|
|
||||||
setError("Exception during document modification");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(" File not open in editor, modifying file directly...");
|
|
||||||
LOG_MESSAGE(" Note: Undo (Ctrl+Z) will not be available for this file until it is opened");
|
|
||||||
|
|
||||||
QFile file(filePath);
|
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
||||||
QString msg = QString("Cannot open file: %1").arg(file.errorString());
|
|
||||||
LOG_MESSAGE(QString(" Failed to open file for reading: %1 - %2")
|
|
||||||
.arg(filePath, file.errorString()));
|
|
||||||
setError(msg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString currentContent = QString::fromUtf8(file.readAll());
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString(" File read successfully (%1 bytes)").arg(currentContent.size()));
|
|
||||||
|
|
||||||
QString modifiedContent = currentContent;
|
|
||||||
QString diffErrorMsg;
|
|
||||||
bool diffSuccess = applyDiffToContent(modifiedContent, diffInfo, reverse, &diffErrorMsg);
|
|
||||||
|
|
||||||
if (!diffSuccess) {
|
|
||||||
LOG_MESSAGE(QString(" Failed to apply diff to file: %1").arg(diffErrorMsg));
|
|
||||||
setError(diffErrorMsg);
|
|
||||||
|
|
||||||
LOG_MESSAGE(" Attempting fallback to old content-based method...");
|
|
||||||
QString oldContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
|
|
||||||
QString newContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
|
|
||||||
|
|
||||||
return performFileEdit(filePath, oldContent, newContent, errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
|
|
||||||
QString msg = QString("Cannot write file: %1").arg(file.errorString());
|
|
||||||
LOG_MESSAGE(QString(" Failed to open file for writing: %1 - %2")
|
|
||||||
.arg(filePath, file.errorString()));
|
|
||||||
setError(msg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextStream out(&file);
|
|
||||||
out << modifiedContent;
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString(" ✓ Successfully wrote modified content to file: %1").arg(filePath));
|
|
||||||
setError(diffErrorMsg);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChangesManager::DiffInfo ChangesManager::createDiffInfo(
|
ChangesManager::DiffInfo ChangesManager::createDiffInfo(
|
||||||
const QString &originalContent,
|
const QString &originalContent,
|
||||||
const QString &modifiedContent,
|
const QString &modifiedContent,
|
||||||
@@ -1390,263 +1089,4 @@ ChangesManager::DiffInfo ChangesManager::createDiffInfo(
|
|||||||
return diffInfo;
|
return diffInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChangesManager::findHunkLocation(
|
|
||||||
const QStringList &fileLines,
|
|
||||||
const DiffHunk &hunk,
|
|
||||||
int &actualStartLine,
|
|
||||||
QString *debugInfo) const
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString(" Searching for hunk location (expected line: %1)").arg(hunk.oldStartLine));
|
|
||||||
|
|
||||||
QString debug;
|
|
||||||
|
|
||||||
int expectedIdx = hunk.oldStartLine - 1;
|
|
||||||
|
|
||||||
if (expectedIdx >= 0 && expectedIdx < fileLines.size()) {
|
|
||||||
bool exactMatch = true;
|
|
||||||
|
|
||||||
int checkIdx = expectedIdx - hunk.contextBefore.size();
|
|
||||||
if (checkIdx < 0) {
|
|
||||||
exactMatch = false;
|
|
||||||
debug += QString(" Context before out of bounds (need %1 lines before line %2)\n")
|
|
||||||
.arg(hunk.contextBefore.size()).arg(expectedIdx + 1);
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < hunk.contextBefore.size(); ++i) {
|
|
||||||
if (fileLines[checkIdx + i] != hunk.contextBefore[i]) {
|
|
||||||
exactMatch = false;
|
|
||||||
debug += QString(" Context before mismatch at offset %1: expected '%2', got '%3'\n")
|
|
||||||
.arg(i).arg(hunk.contextBefore[i]).arg(fileLines[checkIdx + i]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exactMatch) {
|
|
||||||
for (int i = 0; i < hunk.removedLines.size(); ++i) {
|
|
||||||
int lineIdx = expectedIdx + i;
|
|
||||||
if (lineIdx >= fileLines.size() || fileLines[lineIdx] != hunk.removedLines[i]) {
|
|
||||||
exactMatch = false;
|
|
||||||
debug += QString(" Removed line mismatch at offset %1: expected '%2', got '%3'\n")
|
|
||||||
.arg(i)
|
|
||||||
.arg(hunk.removedLines[i])
|
|
||||||
.arg(lineIdx < fileLines.size() ? fileLines[lineIdx] : "<EOF>");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exactMatch && !hunk.contextAfter.isEmpty()) {
|
|
||||||
int afterIdx = expectedIdx + hunk.removedLines.size();
|
|
||||||
for (int i = 0; i < hunk.contextAfter.size(); ++i) {
|
|
||||||
int lineIdx = afterIdx + i;
|
|
||||||
if (lineIdx >= fileLines.size() || fileLines[lineIdx] != hunk.contextAfter[i]) {
|
|
||||||
exactMatch = false;
|
|
||||||
debug += QString(" Context after mismatch at offset %1: expected '%2', got '%3'\n")
|
|
||||||
.arg(i)
|
|
||||||
.arg(hunk.contextAfter[i])
|
|
||||||
.arg(lineIdx < fileLines.size() ? fileLines[lineIdx] : "<EOF>");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exactMatch) {
|
|
||||||
actualStartLine = expectedIdx;
|
|
||||||
LOG_MESSAGE(QString(" ✓ Found exact match at expected line %1").arg(hunk.oldStartLine));
|
|
||||||
if (debugInfo) *debugInfo = "Exact match at expected location";
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
debug += " Exact match at expected location failed, trying fuzzy search...\n";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug += QString(" Expected location %1 is out of bounds (file has %2 lines)\n")
|
|
||||||
.arg(hunk.oldStartLine).arg(fileLines.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(" Trying fuzzy search within ±20 lines...");
|
|
||||||
|
|
||||||
int searchStart = qMax(0, expectedIdx - 20);
|
|
||||||
int searchEnd = qMin(fileLines.size(), expectedIdx + 20);
|
|
||||||
|
|
||||||
int bestMatchLine = -1;
|
|
||||||
int bestMatchScore = 0;
|
|
||||||
|
|
||||||
for (int searchIdx = searchStart; searchIdx < searchEnd; ++searchIdx) {
|
|
||||||
int matchScore = 0;
|
|
||||||
int totalChecks = 0;
|
|
||||||
|
|
||||||
int checkIdx = searchIdx - hunk.contextBefore.size();
|
|
||||||
if (checkIdx >= 0) {
|
|
||||||
for (int i = 0; i < hunk.contextBefore.size(); ++i) {
|
|
||||||
totalChecks++;
|
|
||||||
if (fileLines[checkIdx + i] == hunk.contextBefore[i]) {
|
|
||||||
matchScore++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < hunk.removedLines.size(); ++i) {
|
|
||||||
int lineIdx = searchIdx + i;
|
|
||||||
if (lineIdx < fileLines.size()) {
|
|
||||||
totalChecks++;
|
|
||||||
if (fileLines[lineIdx] == hunk.removedLines[i]) {
|
|
||||||
matchScore++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int afterIdx = searchIdx + hunk.removedLines.size();
|
|
||||||
for (int i = 0; i < hunk.contextAfter.size(); ++i) {
|
|
||||||
int lineIdx = afterIdx + i;
|
|
||||||
if (lineIdx < fileLines.size()) {
|
|
||||||
totalChecks++;
|
|
||||||
if (fileLines[lineIdx] == hunk.contextAfter[i]) {
|
|
||||||
matchScore++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchScore > bestMatchScore) {
|
|
||||||
bestMatchScore = matchScore;
|
|
||||||
bestMatchLine = searchIdx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalPossibleScore = hunk.contextBefore.size() + hunk.removedLines.size() + hunk.contextAfter.size();
|
|
||||||
double matchPercentage = totalPossibleScore > 0 ? (double)bestMatchScore / totalPossibleScore * 100.0 : 0.0;
|
|
||||||
|
|
||||||
if (bestMatchLine != -1 && matchPercentage >= 70.0) {
|
|
||||||
actualStartLine = bestMatchLine;
|
|
||||||
debug += QString(" ✓ Found fuzzy match at line %1 (score: %2/%3 = %4%%)\n")
|
|
||||||
.arg(bestMatchLine + 1)
|
|
||||||
.arg(bestMatchScore)
|
|
||||||
.arg(totalPossibleScore)
|
|
||||||
.arg(matchPercentage, 0, 'f', 1);
|
|
||||||
LOG_MESSAGE(QString(" ✓ Found fuzzy match at line %1 (%2%% confidence)")
|
|
||||||
.arg(bestMatchLine + 1).arg(matchPercentage, 0, 'f', 1));
|
|
||||||
if (debugInfo) *debugInfo = debug;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
debug += QString(" ✗ No suitable match found (best: %1%% at line %2)\n")
|
|
||||||
.arg(matchPercentage, 0, 'f', 1)
|
|
||||||
.arg(bestMatchLine != -1 ? bestMatchLine + 1 : -1);
|
|
||||||
LOG_MESSAGE(QString(" ✗ Hunk location not found (best match: %1%%)").arg(matchPercentage, 0, 'f', 1));
|
|
||||||
|
|
||||||
if (debugInfo) *debugInfo = debug;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChangesManager::applyDiffToContent(
|
|
||||||
QString &content,
|
|
||||||
const DiffInfo &diffInfo,
|
|
||||||
bool reverse,
|
|
||||||
QString *errorMsg)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("=== Applying %1 to content ===").arg(reverse ? "REVERSE diff" : "diff"));
|
|
||||||
|
|
||||||
auto setError = [errorMsg](const QString &msg) {
|
|
||||||
if (errorMsg) *errorMsg = msg;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (diffInfo.useFallback) {
|
|
||||||
LOG_MESSAGE(" Using fallback mode (direct content replacement)");
|
|
||||||
|
|
||||||
QString searchContent = reverse ? diffInfo.modifiedContent : diffInfo.originalContent;
|
|
||||||
QString replaceContent = reverse ? diffInfo.originalContent : diffInfo.modifiedContent;
|
|
||||||
|
|
||||||
int matchPos = content.indexOf(searchContent);
|
|
||||||
if (matchPos != -1) {
|
|
||||||
content = content.left(matchPos)
|
|
||||||
+ replaceContent
|
|
||||||
+ content.mid(matchPos + searchContent.length());
|
|
||||||
setError("Applied using fallback mode (direct replacement)");
|
|
||||||
LOG_MESSAGE(QString(" ✓ Fallback: Direct replacement successful at position %1").arg(matchPos));
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
setError("Fallback failed: Original content not found in file");
|
|
||||||
LOG_MESSAGE(" ✗ Fallback: Content not found");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diffInfo.hunks.isEmpty()) {
|
|
||||||
LOG_MESSAGE(" No hunks to apply (content unchanged)");
|
|
||||||
setError("No changes to apply");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList fileLines = content.split('\n');
|
|
||||||
LOG_MESSAGE(QString(" File has %1 lines, applying %2 hunk(s)")
|
|
||||||
.arg(fileLines.size()).arg(diffInfo.hunks.size()));
|
|
||||||
|
|
||||||
QList<DiffHunk> hunksToApply = diffInfo.hunks;
|
|
||||||
|
|
||||||
std::sort(hunksToApply.begin(), hunksToApply.end(),
|
|
||||||
[](const DiffHunk &a, const DiffHunk &b) {
|
|
||||||
return a.oldStartLine > b.oldStartLine;
|
|
||||||
});
|
|
||||||
|
|
||||||
LOG_MESSAGE(" Hunks sorted in descending order for application");
|
|
||||||
|
|
||||||
int appliedHunks = 0;
|
|
||||||
int failedHunks = 0;
|
|
||||||
|
|
||||||
for (int hunkIdx = 0; hunkIdx < hunksToApply.size(); ++hunkIdx) {
|
|
||||||
const DiffHunk &hunk = hunksToApply[hunkIdx];
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString(" --- Applying hunk %1/%2 ---")
|
|
||||||
.arg(hunkIdx + 1).arg(hunksToApply.size()));
|
|
||||||
|
|
||||||
int actualStartLine = -1;
|
|
||||||
QString debugInfo;
|
|
||||||
|
|
||||||
if (!findHunkLocation(fileLines, hunk, actualStartLine, &debugInfo)) {
|
|
||||||
LOG_MESSAGE(QString(" ✗ Failed to locate hunk %1:\n%2")
|
|
||||||
.arg(hunkIdx + 1).arg(debugInfo));
|
|
||||||
failedHunks++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString(" Applying hunk at line %1 (remove %2 lines, add %3 lines)")
|
|
||||||
.arg(actualStartLine + 1)
|
|
||||||
.arg(hunk.removedLines.size())
|
|
||||||
.arg(hunk.addedLines.size()));
|
|
||||||
|
|
||||||
for (int i = 0; i < hunk.removedLines.size(); ++i) {
|
|
||||||
if (actualStartLine < fileLines.size()) {
|
|
||||||
LOG_MESSAGE(QString(" Removing line %1: '%2'")
|
|
||||||
.arg(actualStartLine + 1)
|
|
||||||
.arg(fileLines[actualStartLine]));
|
|
||||||
fileLines.removeAt(actualStartLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < hunk.addedLines.size(); ++i) {
|
|
||||||
LOG_MESSAGE(QString(" Inserting line %1: '%2'")
|
|
||||||
.arg(actualStartLine + i + 1)
|
|
||||||
.arg(hunk.addedLines[i]));
|
|
||||||
fileLines.insert(actualStartLine + i, hunk.addedLines[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
appliedHunks++;
|
|
||||||
LOG_MESSAGE(QString(" ✓ Hunk %1 applied successfully").arg(hunkIdx + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failedHunks > 0) {
|
|
||||||
QString msg = QString("Partially applied: %1 of %2 hunks succeeded")
|
|
||||||
.arg(appliedHunks).arg(hunksToApply.size());
|
|
||||||
setError(msg);
|
|
||||||
LOG_MESSAGE(QString(" ⚠ %1").arg(msg));
|
|
||||||
|
|
||||||
content = fileLines.join('\n');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
content = fileLines.join('\n');
|
|
||||||
setError(QString("Successfully applied %1 hunk(s)").arg(appliedHunks));
|
|
||||||
LOG_MESSAGE(QString("=== All %1 hunk(s) applied successfully ===").arg(appliedHunks));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public:
|
|||||||
QString lineContent;
|
QString lineContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum FileEditStatus { Pending, Applied, Rejected, Archived };
|
enum FileEditStatus : int { Pending, Applied, Rejected, Archived };
|
||||||
|
|
||||||
struct DiffHunk
|
struct DiffHunk
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,7 @@ public:
|
|||||||
|
|
||||||
void addChange(
|
void addChange(
|
||||||
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
|
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
|
||||||
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
|
QString getRecentChangesContext(const QString ¤tFilePath) const;
|
||||||
|
|
||||||
void addFileEdit(
|
void addFileEdit(
|
||||||
const QString &editId,
|
const QString &editId,
|
||||||
@@ -81,7 +81,6 @@ public:
|
|||||||
bool rejectFileEdit(const QString &editId);
|
bool rejectFileEdit(const QString &editId);
|
||||||
bool undoFileEdit(const QString &editId);
|
bool undoFileEdit(const QString &editId);
|
||||||
FileEdit getFileEdit(const QString &editId) const;
|
FileEdit getFileEdit(const QString &editId) const;
|
||||||
QList<FileEdit> getPendingEdits() const;
|
|
||||||
|
|
||||||
bool applyPendingEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
bool applyPendingEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
||||||
|
|
||||||
@@ -106,13 +105,10 @@ private:
|
|||||||
ChangesManager(const ChangesManager &) = delete;
|
ChangesManager(const ChangesManager &) = delete;
|
||||||
ChangesManager &operator=(const ChangesManager &) = delete;
|
ChangesManager &operator=(const ChangesManager &) = delete;
|
||||||
|
|
||||||
bool performFileEdit(const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg = nullptr);
|
|
||||||
bool performFileEditWithDiff(const QString &filePath, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
|
|
||||||
QString readFileContent(const QString &filePath) const;
|
QString readFileContent(const QString &filePath) const;
|
||||||
|
|
||||||
DiffInfo createDiffInfo(const QString &originalContent, const QString &modifiedContent, const QString &filePath);
|
DiffInfo createDiffInfo(
|
||||||
bool applyDiffToContent(QString &content, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
|
const QString &originalContent, const QString &modifiedContent, const QString &filePath);
|
||||||
bool findHunkLocation(const QStringList &fileLines, const DiffHunk &hunk, int &actualStartLine, QString *debugInfo = nullptr) const;
|
|
||||||
|
|
||||||
// Helper method for fragment-based apply/undo operations
|
// Helper method for fragment-based apply/undo operations
|
||||||
bool performFragmentReplacement(
|
bool performFragmentReplacement(
|
||||||
|
|||||||
@@ -6,25 +6,24 @@
|
|||||||
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
#include "settings/GeneralSettings.hpp"
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
|
||||||
#include <projectexplorer/project.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
|
||||||
#include <projectexplorer/projectnodes.h>
|
|
||||||
#include <texteditor/textdocument.h>
|
|
||||||
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
#include "ProjectScannerQtCreator.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
ContextManager::ContextManager(QObject *parent)
|
ContextManager::ContextManager(QObject *parent)
|
||||||
: QObject(parent)
|
: ContextManager(std::make_unique<ProjectScannerQtCreator>(), parent)
|
||||||
, m_ignoreManager(new IgnoreManager(this))
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
ContextManager::ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_scanner(std::move(scanner))
|
||||||
|
{}
|
||||||
|
|
||||||
|
ContextManager::~ContextManager() = default;
|
||||||
|
|
||||||
QString ContextManager::readFile(const QString &filePath) const
|
QString ContextManager::readFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
@@ -45,9 +44,7 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
|
|||||||
{
|
{
|
||||||
QList<ContentFile> files;
|
QList<ContentFile> files;
|
||||||
for (const QString &path : filePaths) {
|
for (const QString &path : filePaths) {
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(
|
if (m_scanner->shouldIgnore(path)) {
|
||||||
Utils::FilePath::fromString(path));
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(path, project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
|
LOG_MESSAGE(QString("Ignoring file in context due to .qodeassistignore: %1").arg(path));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -58,27 +55,6 @@ QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths)
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList ContextManager::getProjectSourceFiles(ProjectExplorer::Project *project) const
|
|
||||||
{
|
|
||||||
QStringList sourceFiles;
|
|
||||||
if (!project)
|
|
||||||
return sourceFiles;
|
|
||||||
|
|
||||||
auto projectNode = project->rootProjectNode();
|
|
||||||
if (!projectNode)
|
|
||||||
return sourceFiles;
|
|
||||||
|
|
||||||
projectNode->forEachNode(
|
|
||||||
[&sourceFiles, this](ProjectExplorer::FileNode *fileNode) {
|
|
||||||
if (fileNode /*&& shouldProcessFile(fileNode->filePath().toString())*/) {
|
|
||||||
sourceFiles.append(fileNode->filePath().toUrlishString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nullptr);
|
|
||||||
|
|
||||||
return sourceFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
ContentFile ContextManager::createContentFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
ContentFile contentFile;
|
ContentFile contentFile;
|
||||||
@@ -100,77 +76,26 @@ ProgrammingLanguage ContextManager::getDocumentLanguage(const DocumentInfo &docu
|
|||||||
|
|
||||||
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
|
bool ContextManager::isSpecifyCompletion(const DocumentInfo &documentInfo) const
|
||||||
{
|
{
|
||||||
const auto &generalSettings = Settings::generalSettings();
|
Q_UNUSED(documentInfo)
|
||||||
|
return false;
|
||||||
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(documentInfo);
|
|
||||||
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
|
|
||||||
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
|
|
||||||
|
|
||||||
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QPair<QString, QString>> ContextManager::openedFiles(const QStringList excludeFiles) const
|
QString ContextManager::openedFilesContext(const QStringList &excludeFiles) const
|
||||||
{
|
|
||||||
auto documents = Core::DocumentModel::openedDocuments();
|
|
||||||
|
|
||||||
QList<QPair<QString, QString>> files;
|
|
||||||
|
|
||||||
for (const auto *document : std::as_const(documents)) {
|
|
||||||
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
|
|
||||||
if (!textDocument)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto filePath = textDocument->filePath().toUrlishString();
|
|
||||||
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!excludeFiles.contains(filePath)) {
|
|
||||||
files.append({filePath, textDocument->plainText()});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ContextManager::openedFilesContext(const QStringList excludeFiles)
|
|
||||||
{
|
{
|
||||||
QString context = "User files context:\n";
|
QString context = "User files context:\n";
|
||||||
|
|
||||||
auto documents = Core::DocumentModel::openedDocuments();
|
for (const auto &file : m_scanner->openedTextFiles(excludeFiles)) {
|
||||||
|
context += QString("File: %1\n").arg(file.filePath);
|
||||||
for (const auto *document : documents) {
|
context += file.content;
|
||||||
auto textDocument = qobject_cast<const TextEditor::TextDocument *>(document);
|
|
||||||
if (!textDocument)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto filePath = textDocument->filePath().toUrlishString();
|
|
||||||
if (excludeFiles.contains(filePath))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto project = ProjectExplorer::ProjectManager::projectForFile(textDocument->filePath());
|
|
||||||
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Ignoring file in context due to .qodeassistignore: %1").arg(filePath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
context += QString("File: %1\n").arg(filePath);
|
|
||||||
context += textDocument->plainText();
|
|
||||||
|
|
||||||
context += "\n";
|
context += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
IgnoreManager *ContextManager::ignoreManager() const
|
bool ContextManager::shouldIgnore(const QString &filePath) const
|
||||||
{
|
{
|
||||||
return m_ignoreManager;
|
return m_scanner->shouldIgnore(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -4,18 +4,16 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ContentFile.hpp"
|
#include "ContentFile.hpp"
|
||||||
#include "IContextManager.hpp"
|
#include "IContextManager.hpp"
|
||||||
#include "IgnoreManager.hpp"
|
#include "IProjectScanner.hpp"
|
||||||
#include "ProgrammingLanguage.hpp"
|
#include "ProgrammingLanguage.hpp"
|
||||||
|
|
||||||
namespace ProjectExplorer {
|
|
||||||
class Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
class ContextManager : public QObject, public IContextManager
|
class ContextManager : public QObject, public IContextManager
|
||||||
@@ -24,22 +22,22 @@ class ContextManager : public QObject, public IContextManager
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ContextManager(QObject *parent = nullptr);
|
explicit ContextManager(QObject *parent = nullptr);
|
||||||
~ContextManager() override = default;
|
ContextManager(std::unique_ptr<IProjectScanner> scanner, QObject *parent = nullptr);
|
||||||
|
~ContextManager() override;
|
||||||
|
|
||||||
QString readFile(const QString &filePath) const override;
|
QString readFile(const QString &filePath) const override;
|
||||||
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
|
QList<ContentFile> getContentFiles(const QStringList &filePaths) const override;
|
||||||
QStringList getProjectSourceFiles(ProjectExplorer::Project *project) const override;
|
|
||||||
ContentFile createContentFile(const QString &filePath) const override;
|
ContentFile createContentFile(const QString &filePath) const override;
|
||||||
|
|
||||||
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
|
ProgrammingLanguage getDocumentLanguage(const DocumentInfo &documentInfo) const override;
|
||||||
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
|
bool isSpecifyCompletion(const DocumentInfo &documentInfo) const override;
|
||||||
QList<QPair<QString, QString>> openedFiles(const QStringList excludeFiles = QStringList{}) const;
|
|
||||||
QString openedFilesContext(const QStringList excludeFiles = QStringList{});
|
|
||||||
|
|
||||||
IgnoreManager *ignoreManager() const;
|
QString openedFilesContext(const QStringList &excludeFiles = QStringList{}) const;
|
||||||
|
|
||||||
|
bool shouldIgnore(const QString &filePath) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
IgnoreManager *m_ignoreManager;
|
std::unique_ptr<IProjectScanner> m_scanner;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
|
|
||||||
#include "DocumentContextReader.hpp"
|
#include "DocumentContextReader.hpp"
|
||||||
|
|
||||||
#include <languageserverprotocol/lsptypes.h>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
|
|
||||||
#include "CodeCompletionSettings.hpp"
|
#include "CodeCompletionSettings.hpp"
|
||||||
|
|
||||||
#include "ChangesManager.h"
|
#include "ChangesManager.h"
|
||||||
|
#include "EnvBlockFormatter.hpp"
|
||||||
|
|
||||||
const QRegularExpression &getYearRegex()
|
const QRegularExpression &getYearRegex()
|
||||||
{
|
{
|
||||||
@@ -108,15 +107,6 @@ QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosi
|
|||||||
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
|
return getContextBetween(lineNumber, cursorPosition, endLine, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString DocumentContextReader::getLanguageAndFileInfo() const
|
|
||||||
{
|
|
||||||
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_mimeType);
|
|
||||||
QString fileExtension = QFileInfo(m_filePath).suffix();
|
|
||||||
|
|
||||||
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
|
|
||||||
.arg(language, m_mimeType, m_filePath, fileExtension);
|
|
||||||
}
|
|
||||||
|
|
||||||
CopyrightInfo DocumentContextReader::findCopyright()
|
CopyrightInfo DocumentContextReader::findCopyright()
|
||||||
{
|
{
|
||||||
CopyrightInfo result = {-1, -1, false};
|
CopyrightInfo result = {-1, -1, false};
|
||||||
@@ -249,12 +239,7 @@ QString DocumentContextReader::getContextBetween(
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
CopyrightInfo DocumentContextReader::copyrightInfo() const
|
Templates::ContextData DocumentContextReader::prepareContext(
|
||||||
{
|
|
||||||
return m_copyrightInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::ContextData DocumentContextReader::prepareContext(
|
|
||||||
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
|
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const
|
||||||
{
|
{
|
||||||
QString contextBefore;
|
QString contextBefore;
|
||||||
@@ -272,11 +257,13 @@ PluginLLMCore::ContextData DocumentContextReader::prepareContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString fileContext;
|
QString fileContext;
|
||||||
fileContext.append("\n ").append(getLanguageAndFileInfo());
|
fileContext.append("\n")
|
||||||
|
.append(EnvBlockFormatter::formatFile({m_filePath, m_mimeType}))
|
||||||
|
.append("\n");
|
||||||
|
|
||||||
if (settings.useProjectChangesCache())
|
if (settings.useProjectChangesCache())
|
||||||
fileContext.append("Recent Project Changes Context:\n ")
|
fileContext.append("Recent Project Changes Context:\n ")
|
||||||
.append(ChangesManager::instance().getRecentChangesContext(m_textDocument));
|
.append(ChangesManager::instance().getRecentChangesContext(m_filePath));
|
||||||
|
|
||||||
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext};
|
return {.prefix = contextBefore, .suffix = contextAfter, .fileContext = fileContext};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <texteditor/textdocument.h>
|
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
|
||||||
#include <pluginllmcore/ContextData.hpp>
|
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
|
#include <sources/common/ContextData.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
@@ -51,18 +50,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
|
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
|
||||||
|
|
||||||
QString getLanguageAndFileInfo() const;
|
|
||||||
CopyrightInfo findCopyright();
|
CopyrightInfo findCopyright();
|
||||||
QString getContextBetween(
|
QString getContextBetween(
|
||||||
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const;
|
int startLine, int startCursorPosition, int endLine, int endCursorPosition) const;
|
||||||
|
|
||||||
CopyrightInfo copyrightInfo() const;
|
Templates::ContextData prepareContext(
|
||||||
|
|
||||||
PluginLLMCore::ContextData prepareContext(
|
|
||||||
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
|
int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TextEditor::TextDocument *m_textDocument;
|
|
||||||
QTextDocument *m_document;
|
QTextDocument *m_document;
|
||||||
QString m_mimeType;
|
QString m_mimeType;
|
||||||
QString m_filePath;
|
QString m_filePath;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user