mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-15 10:49:18 -04:00
Compare commits
77 Commits
v0.9.12
...
dev-releas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa748b14a | ||
|
|
f499be278d | ||
|
|
231a6a0215 | ||
|
|
69672deb45 | ||
|
|
f36173d932 | ||
|
|
e65ac23e66 | ||
|
|
7bfe9d6f0e | ||
|
|
05fe38e289 | ||
|
|
2c9475cddf | ||
|
|
3179c0c358 | ||
|
|
c151c5030b | ||
|
|
98a618cf87 | ||
|
|
6220308a93 | ||
|
|
02c11ee5a0 | ||
|
|
abb3351246 | ||
|
|
57eeb32ceb | ||
|
|
74eed49fb4 | ||
|
|
43a30281b6 | ||
|
|
bf4307c459 | ||
|
|
6df70e608b | ||
|
|
ee1bf4ffe5 | ||
|
|
aaca9e2a0b | ||
|
|
f2aae9d37f | ||
|
|
dcf5796ad7 | ||
|
|
033c0e8652 | ||
|
|
ea67ba0e2a | ||
|
|
0cf915c4a5 | ||
|
|
99caa853d5 | ||
|
|
278624d412 | ||
|
|
f8adf4d264 | ||
|
|
bfcd8dc1fb | ||
|
|
33321b2499 | ||
|
|
362533a5c0 | ||
|
|
d180d189e4 | ||
|
|
0774084ad9 | ||
|
|
282f48d9fb | ||
|
|
8cbeb7132e | ||
|
|
af898bd255 | ||
|
|
66e25300e8 | ||
|
|
fcc651fd75 | ||
|
|
dc016ce533 | ||
|
|
725de4a2c3 | ||
|
|
8d3313d16b | ||
|
|
abdcab3c7d | ||
|
|
abadc2262c | ||
|
|
31ad99af61 | ||
|
|
fb887967ed | ||
|
|
97236c6069 | ||
|
|
51ebe3e523 | ||
|
|
e193d1e1fa | ||
|
|
ca3baa7597 | ||
|
|
b33a1c2d43 | ||
|
|
c4e34bb3d9 | ||
|
|
b9e0b5a00c | ||
|
|
3f4bda51cd | ||
|
|
7483c78777 | ||
|
|
a3ad314cd4 | ||
|
|
74c899c8c3 | ||
|
|
6addcedfd0 | ||
|
|
eb7fc2f7b4 | ||
|
|
a06320d1c4 | ||
|
|
b1ca6823b8 | ||
|
|
cc2d42f6d7 | ||
|
|
4faeb90dc0 | ||
|
|
9f7497d15c | ||
|
|
cab2f0a55e | ||
|
|
7704bffd88 | ||
|
|
3b421f60af | ||
|
|
86f4635080 | ||
|
|
f21757b9b3 | ||
|
|
9bb6d55687 | ||
|
|
bbb9c47cbb | ||
|
|
46aa53e726 | ||
|
|
4d320bc065 | ||
|
|
7b4e08859c | ||
|
|
d15b46825e | ||
|
|
e0ab5080ea |
4
.github/workflows/build_cmake.yml
vendored
4
.github/workflows/build_cmake.yml
vendored
@@ -50,8 +50,8 @@ jobs:
|
|||||||
qt_creator_version: "18.0.2"
|
qt_creator_version: "18.0.2"
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
qt_version: "6.10.2",
|
qt_version: "6.10.3",
|
||||||
qt_creator_version: "19.0.0"
|
qt_creator_version: "19.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
list(APPEND CMAKE_PREFIX_PATH "/Users/palm1r/Qt/Qt Creator.sdk/lib/cmake/QtCreator")
|
||||||
|
|
||||||
project(QodeAssist)
|
project(QodeAssist)
|
||||||
|
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
@@ -14,7 +16,9 @@ find_package(QtCreator REQUIRED COMPONENTS Core)
|
|||||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
|
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
|
||||||
find_package(GTest)
|
find_package(GTest)
|
||||||
|
|
||||||
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
|
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES
|
||||||
|
en cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk
|
||||||
|
)
|
||||||
|
|
||||||
# IDE_VERSION is defined by QtCreator package
|
# IDE_VERSION is defined by QtCreator package
|
||||||
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
|
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
|
||||||
@@ -34,10 +38,9 @@ add_definitions(
|
|||||||
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(sources/external/llmqore)
|
add_subdirectory(sources)
|
||||||
add_subdirectory(pluginllmcore)
|
|
||||||
add_subdirectory(settings)
|
|
||||||
add_subdirectory(logger)
|
add_subdirectory(logger)
|
||||||
|
add_subdirectory(settings)
|
||||||
add_subdirectory(UIControls)
|
add_subdirectory(UIControls)
|
||||||
add_subdirectory(ChatView)
|
add_subdirectory(ChatView)
|
||||||
add_subdirectory(context)
|
add_subdirectory(context)
|
||||||
@@ -45,6 +48,11 @@ if(GTest_FOUND)
|
|||||||
add_subdirectory(test)
|
add_subdirectory(test)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
option(QODEASSIST_BUILD_BENCH "Build the standalone agent bench CLI" ON)
|
||||||
|
if(QODEASSIST_BUILD_BENCH)
|
||||||
|
add_subdirectory(bench)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_qtc_plugin(QodeAssist
|
add_qtc_plugin(QodeAssist
|
||||||
PLUGIN_DEPENDS
|
PLUGIN_DEPENDS
|
||||||
QtCreator::Core
|
QtCreator::Core
|
||||||
@@ -63,7 +71,9 @@ add_qtc_plugin(QodeAssist
|
|||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::CPlusPlus
|
QtCreator::CPlusPlus
|
||||||
LLMQore
|
LLMQore
|
||||||
PluginLLMCore
|
ProvidersConfig
|
||||||
|
Agents
|
||||||
|
Skills
|
||||||
QodeAssistChatViewplugin
|
QodeAssistChatViewplugin
|
||||||
SOURCES
|
SOURCES
|
||||||
.github/workflows/build_cmake.yml
|
.github/workflows/build_cmake.yml
|
||||||
@@ -74,49 +84,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/DeepSeekCoderFim.hpp
|
|
||||||
# templates/CustomFimTemplate.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/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/OpenAIResponses/ModelRequest.hpp
|
|
||||||
providers/OpenAIResponses/ResponseObject.hpp
|
|
||||||
providers/OpenAIResponses/GetResponseRequest.hpp
|
|
||||||
providers/OpenAIResponses/DeleteResponseRequest.hpp
|
|
||||||
providers/OpenAIResponses/CancelResponseRequest.hpp
|
|
||||||
providers/OpenAIResponses/ListInputItemsRequest.hpp
|
|
||||||
providers/OpenAIResponses/InputTokensRequest.hpp
|
|
||||||
providers/OpenAIResponses/ItemTypesReference.hpp
|
|
||||||
providers/OpenAIResponsesRequestBuilder.hpp
|
|
||||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
@@ -125,7 +92,9 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssistClient.hpp QodeAssistClient.cpp
|
QodeAssistClient.hpp QodeAssistClient.cpp
|
||||||
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
||||||
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
||||||
|
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
||||||
|
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.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
|
||||||
@@ -157,12 +126,16 @@ add_qtc_plugin(QodeAssist
|
|||||||
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
|
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
|
||||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||||
tools/TodoTool.hpp tools/TodoTool.cpp
|
tools/TodoTool.hpp tools/TodoTool.cpp
|
||||||
|
tools/ReadOriginalHistoryTool.hpp tools/ReadOriginalHistoryTool.cpp
|
||||||
|
tools/SkillTool.hpp tools/SkillTool.cpp
|
||||||
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
|
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
|
||||||
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
|
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
|
||||||
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
|
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
|
||||||
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
|
||||||
|
|
||||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||||
find_program(QtCreatorExecutable
|
find_program(QtCreatorExecutable
|
||||||
NAMES
|
NAMES
|
||||||
@@ -184,5 +157,5 @@ endif()
|
|||||||
qt_add_translations(TARGETS QodeAssist
|
qt_add_translations(TARGETS QodeAssist
|
||||||
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
|
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
|
||||||
RESOURCE_PREFIX "/translations"
|
RESOURCE_PREFIX "/translations"
|
||||||
LUPDATE_OPTIONS -no-obsolete
|
LUPDATE_OPTIONS -no-obsolete -locations none
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
qml/controls/FileMentionPopup.qml
|
qml/controls/FileMentionPopup.qml
|
||||||
qml/controls/FileEditsActionBar.qml
|
qml/controls/FileEditsActionBar.qml
|
||||||
qml/controls/ContextViewer.qml
|
qml/controls/ContextViewer.qml
|
||||||
|
qml/controls/SkillCommandPopup.qml
|
||||||
qml/controls/Toast.qml
|
qml/controls/Toast.qml
|
||||||
qml/controls/TopBar.qml
|
qml/controls/TopBar.qml
|
||||||
qml/controls/SplitDropZone.qml
|
qml/controls/SplitDropZone.qml
|
||||||
|
qml/controls/MessageNavigator.qml
|
||||||
|
|
||||||
RESOURCES
|
RESOURCES
|
||||||
icons/attach-file-light.svg
|
icons/attach-file-light.svg
|
||||||
@@ -43,9 +45,12 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/window-unlock.svg
|
icons/window-unlock.svg
|
||||||
icons/chat-icon.svg
|
icons/chat-icon.svg
|
||||||
icons/chat-pause-icon.svg
|
icons/chat-pause-icon.svg
|
||||||
|
icons/warning-icon.svg
|
||||||
|
icons/new-chat-icon.svg
|
||||||
icons/rules-icon.svg
|
icons/rules-icon.svg
|
||||||
icons/context-icon.svg
|
icons/context-icon.svg
|
||||||
icons/open-in-editor.svg
|
icons/open-in-editor.svg
|
||||||
|
icons/open-in-window.svg
|
||||||
icons/apply-changes-button.svg
|
icons/apply-changes-button.svg
|
||||||
icons/undo-changes-button.svg
|
icons/undo-changes-button.svg
|
||||||
icons/reject-changes-button.svg
|
icons/reject-changes-button.svg
|
||||||
@@ -55,6 +60,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/tools-icon-off.svg
|
icons/tools-icon-off.svg
|
||||||
icons/settings-icon.svg
|
icons/settings-icon.svg
|
||||||
icons/compress-icon.svg
|
icons/compress-icon.svg
|
||||||
|
icons/open-in-code.svg
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
@@ -69,7 +75,12 @@ 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
|
||||||
|
ChatAgentController.hpp ChatAgentController.cpp
|
||||||
|
FileEditController.hpp FileEditController.cpp
|
||||||
|
InputTokenCounter.hpp InputTokenCounter.cpp
|
||||||
|
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
||||||
FileMentionItem.hpp FileMentionItem.cpp
|
FileMentionItem.hpp FileMentionItem.cpp
|
||||||
|
SessionFileRegistry.hpp SessionFileRegistry.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
@@ -80,14 +91,16 @@ 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
|
||||||
|
Agents
|
||||||
|
Session
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
|
|||||||
105
ChatView/ChatAgentController.cpp
Normal file
105
ChatView/ChatAgentController.cpp
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "ChatAgentController.hpp"
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <AgentConfig.hpp>
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <sources/settings/PipelinesConfig.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatAgentController::ChatAgentController(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
if (auto *settings = Core::ICore::settings())
|
||||||
|
m_currentAgent = settings->value(kChatAgentKey).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatAgentController::setAgentFactory(AgentFactory *factory)
|
||||||
|
{
|
||||||
|
m_agentFactory = factory;
|
||||||
|
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;
|
||||||
|
|
||||||
|
m_currentAgent = name;
|
||||||
|
if (auto *settings = Core::ICore::settings())
|
||||||
|
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||||
|
emit currentAgentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatAgentController::reload()
|
||||||
|
{
|
||||||
|
const QStringList all = m_agentFactory ? m_agentFactory->configNames() : QStringList{};
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatAssistant;
|
||||||
|
|
||||||
|
if (roster.isEmpty()) {
|
||||||
|
m_availableAgents = all;
|
||||||
|
} else {
|
||||||
|
QStringList filtered;
|
||||||
|
for (const QString &name : roster) {
|
||||||
|
if (all.contains(name))
|
||||||
|
filtered.append(name);
|
||||||
|
}
|
||||||
|
m_availableAgents = filtered.isEmpty() ? all : 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;
|
||||||
|
|
||||||
|
m_currentAgent = next;
|
||||||
|
if (auto *settings = Core::ICore::settings())
|
||||||
|
settings->setValue(kChatAgentKey, m_currentAgent);
|
||||||
|
emit currentAgentChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatAgentController::currentSupportsThinking() const
|
||||||
|
{
|
||||||
|
if (!m_agentFactory || m_currentAgent.isEmpty())
|
||||||
|
return false;
|
||||||
|
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
|
||||||
|
return config && config->enableThinking;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
47
ChatView/ChatAgentController.hpp
Normal file
47
ChatView/ChatAgentController.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatAgentController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatAgentController(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setAgentFactory(AgentFactory *factory);
|
||||||
|
|
||||||
|
QStringList availableAgents() const;
|
||||||
|
QString currentAgent() const;
|
||||||
|
void setCurrentAgent(const QString &name);
|
||||||
|
|
||||||
|
bool currentSupportsThinking() const;
|
||||||
|
bool currentSupportsTools() const;
|
||||||
|
|
||||||
|
void reload();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void availableAgentsChanged();
|
||||||
|
void currentAgentChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ensureValidCurrent();
|
||||||
|
|
||||||
|
QPointer<AgentFactory> m_agentFactory;
|
||||||
|
QStringList m_availableAgents;
|
||||||
|
QString m_currentAgent;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "ChatCompressor.hpp"
|
#include "ChatCompressor.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include "ChatModel.hpp"
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "PromptTemplateManager.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -24,7 +32,18 @@ ChatCompressor::ChatCompressor(QObject *parent)
|
|||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
void ChatCompressor::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::setActiveAgent(const QString &agentName)
|
||||||
|
{
|
||||||
|
m_activeAgent = agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::startCompression(
|
||||||
|
const QString &chatFilePath, ConversationHistory *sourceHistory)
|
||||||
{
|
{
|
||||||
if (m_isCompressing) {
|
if (m_isCompressing) {
|
||||||
emit compressionFailed(tr("Compression already in progress"));
|
emit compressionFailed(tr("Compression already in progress"));
|
||||||
@@ -36,47 +55,78 @@ 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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!promptTemplate) {
|
auto *client = session->client();
|
||||||
emit compressionFailed(tr("No template available"));
|
if (!client) {
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emit compressionFailed(tr("Chat agent has no live client"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
session->systemPrompt()->setLayer(
|
||||||
|
QStringLiteral("compression"),
|
||||||
|
QStringLiteral(
|
||||||
|
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||||
|
"Your summaries preserve key information, technical details, and the flow of "
|
||||||
|
"discussion."));
|
||||||
|
|
||||||
QJsonObject payload{
|
auto *history = session->history();
|
||||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
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;
|
||||||
|
|
||||||
buildRequestPayload(payload, promptTemplate);
|
Message apiMessage(msg.role());
|
||||||
|
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(text));
|
||||||
|
history->append(std::move(apiMessage));
|
||||||
|
}
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
connect(
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
session, &Session::finished, this,
|
||||||
: promptTemplate->endpoint();
|
[this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); });
|
||||||
m_currentRequestId = m_provider->sendRequest(
|
connect(
|
||||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
session, &Session::failed, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onCompressionFailed(id, error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
client->setTransferTimeout(
|
||||||
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(buildCompressionPrompt()));
|
||||||
|
|
||||||
|
m_currentRequestId = session->send(std::move(blocks), /*toolsOverride=*/false);
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
handleCompressionError(tr("Failed to start compression request: %1")
|
||||||
|
.arg(session->lastError().message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,44 +141,38 @@ 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)
|
const QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||||
return;
|
const QString sourcePath = m_originalChatPath;
|
||||||
|
|
||||||
LOG_MESSAGE(
|
cleanupState();
|
||||||
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
|
||||||
|
|
||||||
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) {
|
||||||
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
emit compressionFailed(tr("Failed to save compressed chat"));
|
||||||
handleCompressionError(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;
|
||||||
@@ -165,39 +209,6 @@ QString ChatCompressor::buildCompressionPrompt() const
|
|||||||
"Create the summary now:");
|
"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)
|
||||||
{
|
{
|
||||||
@@ -221,13 +232,15 @@ bool ChatCompressor::createCompressedChatFile(
|
|||||||
|
|
||||||
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["compressedAt"] = QDateTime::currentDateTime().toString(Qt::ISODate);
|
||||||
|
|
||||||
if (QFile::exists(destPath))
|
if (QFile::exists(destPath))
|
||||||
QFile::remove(destPath);
|
QFile::remove(destPath);
|
||||||
@@ -242,49 +255,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
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
namespace QodeAssist::PluginLLMCore {
|
namespace QodeAssist {
|
||||||
class Provider;
|
class SessionManager;
|
||||||
class PromptTemplate;
|
class Session;
|
||||||
} // namespace QodeAssist::PluginLLMCore
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class ChatCompressor : public QObject
|
class ChatCompressor : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -24,7 +24,10 @@ class ChatCompressor : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit ChatCompressor(QObject *parent = nullptr);
|
explicit ChatCompressor(QObject *parent = nullptr);
|
||||||
|
|
||||||
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
void setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setActiveAgent(const QString &agentName);
|
||||||
|
|
||||||
|
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
|
||||||
|
|
||||||
bool isCompressing() const;
|
bool isCompressing() const;
|
||||||
void cancelCompression();
|
void cancelCompression();
|
||||||
@@ -34,30 +37,23 @@ 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;
|
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,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "ChatFileManager.hpp"
|
#include "ChatFileManager.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
240
ChatView/ChatHistoryStore.cpp
Normal file
240
ChatView/ChatHistoryStore.cpp
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include "ChatHistoryStore.hpp"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "ProjectSettings.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_history(history)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::historyDir() const
|
||||||
|
{
|
||||||
|
QString path;
|
||||||
|
|
||||||
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
|
} else {
|
||||||
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists() && !dir.mkpath(".")) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::suggestedFileName() const
|
||||||
|
{
|
||||||
|
QString shortMessage;
|
||||||
|
|
||||||
|
if (m_history) {
|
||||||
|
for (const auto &message : m_history->messages()) {
|
||||||
|
if (message.role() != Message::Role::User)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const QString text = message.text();
|
||||||
|
if (!text.trimmed().isEmpty()) {
|
||||||
|
shortMessage = text.split('\n').first().simplified().left(30);
|
||||||
|
} else {
|
||||||
|
for (const auto &block : message.blocks()) {
|
||||||
|
if (dynamic_cast<StoredImageContent *>(block.get())) {
|
||||||
|
shortMessage = "image_chat";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateChatFileName(shortMessage, historyDir());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::autosaveFilePath(const QString &recentFilePath) const
|
||||||
|
{
|
||||||
|
if (!recentFilePath.isEmpty()) {
|
||||||
|
return recentFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString dir = historyDir();
|
||||||
|
if (dir.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QDir(dir).filePath(suggestedFileName() + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::autosaveFilePath(
|
||||||
|
const QString &recentFilePath, const QString &firstMessage, bool hasImageAttachments) const
|
||||||
|
{
|
||||||
|
if (!recentFilePath.isEmpty()) {
|
||||||
|
return recentFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString dir = historyDir();
|
||||||
|
if (dir.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||||
|
|
||||||
|
if (shortMessage.isEmpty() && hasImageAttachments) {
|
||||||
|
shortMessage = "image_chat";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString fileName = generateChatFileName(shortMessage, dir);
|
||||||
|
return QDir(dir).filePath(fileName + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
||||||
|
{
|
||||||
|
return ChatSerializer::saveToFile(m_history, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
||||||
|
{
|
||||||
|
return ChatSerializer::loadFromFile(m_history, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatHistoryStore::showSaveDialog()
|
||||||
|
{
|
||||||
|
QString initialDir = historyDir();
|
||||||
|
|
||||||
|
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
|
||||||
|
dialog->setAcceptMode(QFileDialog::AcceptSave);
|
||||||
|
dialog->setFileMode(QFileDialog::AnyFile);
|
||||||
|
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||||
|
dialog->setDefaultSuffix("json");
|
||||||
|
if (!initialDir.isEmpty()) {
|
||||||
|
dialog->setDirectory(initialDir);
|
||||||
|
dialog->selectFile(suggestedFileName() + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||||
|
if (result == QFileDialog::Accepted) {
|
||||||
|
QStringList files = dialog->selectedFiles();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
emit saveRequested(files.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatHistoryStore::showLoadDialog()
|
||||||
|
{
|
||||||
|
QString initialDir = historyDir();
|
||||||
|
|
||||||
|
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
|
||||||
|
dialog->setAcceptMode(QFileDialog::AcceptOpen);
|
||||||
|
dialog->setFileMode(QFileDialog::ExistingFile);
|
||||||
|
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||||
|
if (!initialDir.isEmpty()) {
|
||||||
|
dialog->setDirectory(initialDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||||
|
if (result == QFileDialog::Accepted) {
|
||||||
|
QStringList files = dialog->selectedFiles();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
emit loadRequested(files.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatHistoryStore::openHistoryFolder() const
|
||||||
|
{
|
||||||
|
QString path;
|
||||||
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
|
} else {
|
||||||
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkpath(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||||
|
QDesktopServices::openUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::generateChatFileName(const QString &shortMessage, const QString &dir) const
|
||||||
|
{
|
||||||
|
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
|
||||||
|
static const QRegularExpression underSymbols = QRegularExpression("_+");
|
||||||
|
|
||||||
|
QStringList parts;
|
||||||
|
QString sanitizedMessage = shortMessage;
|
||||||
|
sanitizedMessage.replace(saitizeSymbols, "_");
|
||||||
|
sanitizedMessage.replace(underSymbols, "_");
|
||||||
|
sanitizedMessage = sanitizedMessage.trimmed();
|
||||||
|
|
||||||
|
if (!sanitizedMessage.isEmpty()) {
|
||||||
|
if (sanitizedMessage.startsWith('_')) {
|
||||||
|
sanitizedMessage.remove(0, 1);
|
||||||
|
}
|
||||||
|
if (sanitizedMessage.endsWith('_')) {
|
||||||
|
sanitizedMessage.chop(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString fullPath = QDir(dir).filePath(sanitizedMessage);
|
||||||
|
QFileInfo fileInfo(fullPath);
|
||||||
|
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
|
||||||
|
parts << sanitizedMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
|
||||||
|
|
||||||
|
QString fileName = parts.join("_");
|
||||||
|
QString fullPath = QDir(dir).filePath(fileName);
|
||||||
|
QFileInfo finalCheck(fullPath);
|
||||||
|
|
||||||
|
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
||||||
|
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
50
ChatView/ChatHistoryStore.hpp
Normal file
50
ChatView/ChatHistoryStore.hpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// 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 <QString>
|
||||||
|
|
||||||
|
#include "ChatSerializer.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatHistoryStore : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString historyDir() const;
|
||||||
|
QString suggestedFileName() const;
|
||||||
|
QString autosaveFilePath(const QString &recentFilePath) const;
|
||||||
|
QString autosaveFilePath(
|
||||||
|
const QString &recentFilePath,
|
||||||
|
const QString &firstMessage,
|
||||||
|
bool hasImageAttachments) const;
|
||||||
|
|
||||||
|
SerializationResult save(const QString &filePath) const;
|
||||||
|
SerializationResult load(const QString &filePath) const;
|
||||||
|
|
||||||
|
void showSaveDialog();
|
||||||
|
void showLoadDialog();
|
||||||
|
void openHistoryFolder() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void saveRequested(const QString &filePath);
|
||||||
|
void loadRequested(const QString &filePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||||
|
|
||||||
|
ConversationHistory *m_history;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -7,107 +8,133 @@
|
|||||||
#include "MessagePart.hpp"
|
#include "MessagePart.hpp"
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
#include <QHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QVector>
|
||||||
#include <QtQmlIntegration>
|
#include <QtQmlIntegration>
|
||||||
|
|
||||||
#include "context/ContentFile.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatModel : public QAbstractListModel
|
class ChatModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
|
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||||
|
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
|
||||||
|
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||||
|
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||||
Q_ENUM(ChatRole)
|
Q_ENUM(ChatRole)
|
||||||
|
|
||||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted, Images };
|
enum Roles {
|
||||||
|
RoleType = Qt::UserRole,
|
||||||
|
Content,
|
||||||
|
Attachments,
|
||||||
|
IsRedacted,
|
||||||
|
Images,
|
||||||
|
PromptTokens,
|
||||||
|
CompletionTokens,
|
||||||
|
CachedPromptTokens,
|
||||||
|
ReasoningTokens,
|
||||||
|
TotalTokens
|
||||||
|
};
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
int tokensThreshold() 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;
|
||||||
|
|
||||||
void addToolExecutionStatus(
|
void setMessageUsage(
|
||||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
const QString &messageId,
|
||||||
void updateToolResult(
|
int promptTokens,
|
||||||
const QString &requestId,
|
int completionTokens,
|
||||||
const QString &toolId,
|
int cachedPromptTokens,
|
||||||
const QString &toolName,
|
int reasoningTokens);
|
||||||
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 setLoadingFromHistory(bool loading);
|
int sessionPromptTokens() const;
|
||||||
bool isLoadingFromHistory() const;
|
int sessionCompletionTokens() const;
|
||||||
|
int sessionCachedPromptTokens() const;
|
||||||
|
int sessionTotalTokens() const;
|
||||||
|
|
||||||
void setChatFilePath(const QString &filePath);
|
void setChatFilePath(const QString &filePath);
|
||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void tokensThresholdChanged();
|
|
||||||
void modelReseted();
|
void modelReseted();
|
||||||
|
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;
|
||||||
|
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 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, 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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,41 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPointer>
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
|
|
||||||
#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 {
|
||||||
|
class SkillsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class AgentFactory;
|
||||||
|
class SessionManager;
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatCompressor;
|
class ChatCompressor;
|
||||||
|
class ChatAgentController;
|
||||||
|
class FileEditController;
|
||||||
|
class InputTokenCounter;
|
||||||
|
class ChatHistoryStore;
|
||||||
|
class SessionFileRegistry;
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
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)
|
||||||
@@ -34,32 +49,30 @@ 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 useThinking READ useThinking NOTIFY useThinkingChanged FINAL)
|
||||||
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
||||||
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
|
||||||
|
|
||||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
|
||||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
|
||||||
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
Q_PROPERTY(QStringList availableRoles READ availableRoles NOTIFY availableRolesChanged FINAL)
|
||||||
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
Q_PROPERTY(QString currentRole READ currentRole WRITE setCurrentRole NOTIFY currentRoleChanged FINAL)
|
||||||
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
|
||||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
|
||||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
|
||||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
|
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
||||||
|
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ChatRootView(QQuickItem *parent = nullptr);
|
ChatRootView(QQuickItem *parent = nullptr);
|
||||||
|
~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);
|
||||||
@@ -84,13 +97,19 @@ public:
|
|||||||
Q_INVOKABLE void showAddImageDialog();
|
Q_INVOKABLE void showAddImageDialog();
|
||||||
Q_INVOKABLE bool isImageFile(const QString &filePath) const;
|
Q_INVOKABLE bool isImageFile(const QString &filePath) const;
|
||||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||||
|
Q_INVOKABLE bool isSendShortcut(int key, int modifiers) const;
|
||||||
|
QString sendShortcutText() const;
|
||||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
Q_INVOKABLE void openChatHistoryFolder();
|
Q_INVOKABLE void openChatHistoryFolder();
|
||||||
Q_INVOKABLE void openRulesFolder();
|
|
||||||
Q_INVOKABLE void openSettings();
|
Q_INVOKABLE void openSettings();
|
||||||
|
|
||||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
|
|
||||||
|
Q_INVOKABLE void relocateToSplit();
|
||||||
|
Q_INVOKABLE void relocateToWindow();
|
||||||
|
|
||||||
|
void consumePendingChatFile();
|
||||||
|
|
||||||
Q_INVOKABLE void updateInputTokensCount();
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
|
|
||||||
@@ -117,15 +136,10 @@ public:
|
|||||||
|
|
||||||
QString lastErrorMessage() const;
|
QString lastErrorMessage() const;
|
||||||
|
|
||||||
QVariantList activeRules() const;
|
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
||||||
int activeRulesCount() const;
|
|
||||||
Q_INVOKABLE QString getRuleContent(int index);
|
|
||||||
Q_INVOKABLE void refreshRules();
|
|
||||||
|
|
||||||
bool useTools() const;
|
bool useTools() const;
|
||||||
void setUseTools(bool enabled);
|
|
||||||
bool useThinking() const;
|
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);
|
||||||
@@ -136,22 +150,18 @@ public:
|
|||||||
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||||
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
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;
|
Q_INVOKABLE void loadAvailableRoles();
|
||||||
QString currentAgentRoleDescription() const;
|
QStringList availableRoles() const;
|
||||||
QString currentAgentRoleSystemPrompt() const;
|
QString currentRole() const;
|
||||||
|
void setCurrentRole(const QString &roleId);
|
||||||
|
|
||||||
int currentMessageTotalEdits() const;
|
int currentMessageTotalEdits() const;
|
||||||
int currentMessageAppliedEdits() const;
|
int currentMessageAppliedEdits() const;
|
||||||
@@ -164,6 +174,13 @@ public:
|
|||||||
|
|
||||||
bool isCompressing() const;
|
bool isCompressing() const;
|
||||||
|
|
||||||
|
bool isInEditor() const;
|
||||||
|
void setInEditor(bool value);
|
||||||
|
|
||||||
|
QString chatTitle() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void requestNewChat();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
void copyToClipboard(const QString &text);
|
void copyToClipboard(const QString &text);
|
||||||
@@ -174,7 +191,6 @@ public slots:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void chatModelChanged();
|
void chatModelChanged();
|
||||||
void currentTemplateChanged();
|
|
||||||
void attachmentFilesChanged();
|
void attachmentFilesChanged();
|
||||||
void linkedFilesChanged();
|
void linkedFilesChanged();
|
||||||
void inputTokensCountChanged();
|
void inputTokensCountChanged();
|
||||||
@@ -190,64 +206,87 @@ signals:
|
|||||||
|
|
||||||
void lastErrorMessageChanged();
|
void lastErrorMessageChanged();
|
||||||
void lastInfoMessageChanged();
|
void lastInfoMessageChanged();
|
||||||
void activeRulesChanged();
|
void sendShortcutTextChanged();
|
||||||
void activeRulesCountChanged();
|
|
||||||
|
|
||||||
void useToolsChanged();
|
void useToolsChanged();
|
||||||
void useThinkingChanged();
|
void useThinkingChanged();
|
||||||
void currentMessageEditsStatsChanged();
|
void currentMessageEditsStatsChanged();
|
||||||
|
|
||||||
void isThinkingSupportChanged();
|
void isThinkingSupportChanged();
|
||||||
void availableConfigurationsChanged();
|
|
||||||
void currentConfigurationChanged();
|
|
||||||
|
|
||||||
void availableAgentRolesChanged();
|
void availableChatAgentsChanged();
|
||||||
void currentAgentRoleChanged();
|
void currentChatAgentChanged();
|
||||||
void baseSystemPromptChanged();
|
void availableRolesChanged();
|
||||||
|
void currentRoleChanged();
|
||||||
|
|
||||||
void isCompressingChanged();
|
void isCompressingChanged();
|
||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
void compressionFailed(const QString &error);
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
void isInEditorChanged();
|
||||||
|
void chatTitleChanged();
|
||||||
|
|
||||||
void openFilesChanged();
|
void openFilesChanged();
|
||||||
|
|
||||||
|
void closeHostRequested();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
QString computeChatTitle() const;
|
||||||
QString getChatsHistoryDir() const;
|
void triggerOpenChatCommand(Utils::Id commandId);
|
||||||
QString getSuggestedFileName() const;
|
void handOffSession();
|
||||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
bool deferSendForAutoCompress(
|
||||||
|
const QString &message,
|
||||||
|
const QStringList &attachments,
|
||||||
|
const QStringList &linkedFiles);
|
||||||
|
void dispatchSend(
|
||||||
|
const QString &message,
|
||||||
|
const QStringList &attachments,
|
||||||
|
const QStringList &linkedFiles);
|
||||||
bool hasImageAttachments(const QStringList &attachments) const;
|
bool hasImageAttachments(const QStringList &attachments) const;
|
||||||
|
|
||||||
|
SessionFileRegistry *sessionFileRegistry() const;
|
||||||
|
Skills::SkillsManager *skillsManager() const;
|
||||||
|
AgentFactory *agentFactory() const;
|
||||||
|
SessionManager *sessionManager() const;
|
||||||
|
|
||||||
|
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;
|
||||||
int m_messageTokensCount{0};
|
|
||||||
int m_inputTokensCount{0};
|
struct PendingSend {
|
||||||
|
QString message;
|
||||||
|
QStringList attachments;
|
||||||
|
QStringList linkedFiles;
|
||||||
|
bool active = false;
|
||||||
|
};
|
||||||
|
PendingSend m_pendingSend;
|
||||||
bool m_isSyncOpenFiles;
|
bool m_isSyncOpenFiles;
|
||||||
|
bool m_isInEditor = false;
|
||||||
|
mutable QString m_cachedChatTitle;
|
||||||
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_currentMessageRequestId;
|
|
||||||
int m_currentMessageTotalEdits{0};
|
|
||||||
int m_currentMessageAppliedEdits{0};
|
|
||||||
int m_currentMessagePendingEdits{0};
|
|
||||||
int m_currentMessageRejectedEdits{0};
|
|
||||||
QString m_lastInfoMessage;
|
QString m_lastInfoMessage;
|
||||||
|
|
||||||
QStringList m_availableConfigurations;
|
QString m_currentRole = QStringLiteral("developer");
|
||||||
QString m_currentConfiguration;
|
QStringList m_availableRoles;
|
||||||
|
|
||||||
QStringList m_availableAgentRoles;
|
|
||||||
QString m_currentAgentRole;
|
|
||||||
|
|
||||||
ChatCompressor *m_chatCompressor;
|
ChatCompressor *m_chatCompressor;
|
||||||
|
ChatAgentController *m_agentController;
|
||||||
|
FileEditController *m_fileEditController;
|
||||||
|
InputTokenCounter *m_tokenCounter;
|
||||||
|
ChatHistoryStore *m_historyStore;
|
||||||
|
mutable QPointer<SessionFileRegistry> m_sessionFileRegistry;
|
||||||
|
mutable bool m_sessionFileRegistryResolved = false;
|
||||||
|
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
||||||
|
mutable bool m_skillsManagerResolved = false;
|
||||||
|
mutable QPointer<AgentFactory> m_agentFactory;
|
||||||
|
mutable QPointer<SessionManager> m_sessionManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
|
||||||
#include <QBuffer>
|
#include <memory>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -12,12 +14,57 @@
|
|||||||
#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:");
|
||||||
|
|
||||||
|
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
|
||||||
|
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
|
||||||
|
|
||||||
|
void registerEditFromResult(const QString &result)
|
||||||
{
|
{
|
||||||
|
const int pos = result.indexOf(kFileEditMarker);
|
||||||
|
if (pos < 0)
|
||||||
|
return;
|
||||||
|
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||||
|
if (!doc.isObject())
|
||||||
|
return;
|
||||||
|
const QJsonObject obj = doc.object();
|
||||||
|
const QString editId = obj.value("edit_id").toString();
|
||||||
|
const QString filePath = obj.value("file").toString();
|
||||||
|
if (editId.isEmpty() || filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
Context::ChangesManager::instance().addFileEdit(
|
||||||
|
editId,
|
||||||
|
filePath,
|
||||||
|
obj.value("old_content").toString(),
|
||||||
|
obj.value("new_content").toString(),
|
||||||
|
/*autoApply=*/false,
|
||||||
|
/*isFromHistory=*/true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const QString ChatSerializer::VERSION = "0.3";
|
||||||
|
|
||||||
|
SerializationResult ChatSerializer::saveToFile(
|
||||||
|
const ConversationHistory *history, const QString &filePath)
|
||||||
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
if (!ensureDirectoryExists(filePath)) {
|
if (!ensureDirectoryExists(filePath)) {
|
||||||
return {false, "Failed to create directory structure"};
|
return {false, "Failed to create directory structure"};
|
||||||
}
|
}
|
||||||
@@ -27,9 +74,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = serializeChat(model, filePath);
|
QJsonDocument doc(serializeChat(history));
|
||||||
QJsonDocument doc(root);
|
|
||||||
|
|
||||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||||
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
|
||||||
}
|
}
|
||||||
@@ -37,8 +82,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)
|
||||||
{
|
{
|
||||||
|
if (!history)
|
||||||
|
return {false, "No conversation history"};
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
|
||||||
@@ -50,145 +99,140 @@ 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);
|
||||||
|
return loadLegacy(history, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {true, QString()};
|
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history)
|
||||||
}
|
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeMessage(
|
|
||||||
const ChatModel::Message &message, const QString &chatFilePath)
|
|
||||||
{
|
|
||||||
QJsonObject messageObj;
|
|
||||||
messageObj["role"] = static_cast<int>(message.role);
|
|
||||||
messageObj["content"] = message.content;
|
|
||||||
messageObj["id"] = message.id;
|
|
||||||
|
|
||||||
if (message.isRedacted) {
|
|
||||||
messageObj["isRedacted"] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.signature.isEmpty()) {
|
|
||||||
messageObj["signature"] = message.signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::deserializeChat(
|
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root)
|
||||||
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray = json["messages"].toArray();
|
history->clear();
|
||||||
QVector<ChatModel::Message> messages;
|
|
||||||
messages.reserve(messagesArray.size());
|
|
||||||
|
|
||||||
for (const auto &messageValue : messagesArray) {
|
const QJsonArray messagesArray = root["messages"].toArray();
|
||||||
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
|
for (const auto &value : messagesArray) {
|
||||||
|
bool ok = false;
|
||||||
|
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
|
||||||
|
if (ok)
|
||||||
|
history->append(std::move(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
model->clear();
|
registerHistoricalFileEdits(history);
|
||||||
|
return {true, QString()};
|
||||||
model->setLoadingFromHistory(true);
|
|
||||||
|
|
||||||
for (const auto &message : messages) {
|
|
||||||
model->addMessage(
|
|
||||||
message.content,
|
|
||||||
message.role,
|
|
||||||
message.id,
|
|
||||||
message.attachments,
|
|
||||||
message.images,
|
|
||||||
message.isRedacted,
|
|
||||||
message.signature);
|
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
|
||||||
.arg(message.images.size())
|
|
||||||
.arg(message.isRedacted)
|
|
||||||
.arg(message.signature.length()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(false);
|
SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root)
|
||||||
|
{
|
||||||
|
history->clear();
|
||||||
|
|
||||||
return true;
|
const QJsonArray arr = root["messages"].toArray();
|
||||||
|
int i = 0;
|
||||||
|
while (i < arr.size()) {
|
||||||
|
const QJsonObject mj = arr[i].toObject();
|
||||||
|
const auto role = static_cast<LegacyRole>(mj["role"].toInt());
|
||||||
|
|
||||||
|
if (role == LegacyRole::Tool) {
|
||||||
|
Message assistant(Message::Role::Assistant);
|
||||||
|
Message toolResults(Message::Role::User);
|
||||||
|
while (i < arr.size()
|
||||||
|
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) {
|
||||||
|
const QJsonObject tj = arr[i].toObject();
|
||||||
|
const QString toolName = tj["toolName"].toString();
|
||||||
|
const QString id = tj["id"].toString();
|
||||||
|
if (!toolName.isEmpty()) {
|
||||||
|
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
|
||||||
|
id, toolName, tj["toolArguments"].toObject()));
|
||||||
|
toolResults.appendBlock(std::make_unique<LLMQore::ToolResultContent>(
|
||||||
|
id, tj["toolResult"].toString()));
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (!assistant.blocks().empty()) {
|
||||||
|
history->append(std::move(assistant));
|
||||||
|
history->append(std::move(toolResults));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++i;
|
||||||
|
|
||||||
|
if (role == LegacyRole::FileEdit)
|
||||||
|
continue; // derived from the tool result in the new model
|
||||||
|
|
||||||
|
if (role == LegacyRole::Thinking) {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
const QString signature = mj["signature"].toString();
|
||||||
|
Message assistant(Message::Role::Assistant);
|
||||||
|
if (mj["isRedacted"].toBool(false)) {
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::RedactedThinkingContent>(signature));
|
||||||
|
} else {
|
||||||
|
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
|
||||||
|
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
|
||||||
|
assistant.appendBlock(
|
||||||
|
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
|
||||||
|
}
|
||||||
|
history->append(std::move(assistant));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == LegacyRole::User) {
|
||||||
|
Message user(Message::Role::User, mj["id"].toString());
|
||||||
|
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
|
||||||
|
for (const auto &a : mj["attachments"].toArray()) {
|
||||||
|
const QJsonObject ao = a.toObject();
|
||||||
|
user.appendBlock(std::make_unique<StoredAttachmentContent>(
|
||||||
|
ao["fileName"].toString(), ao["storedPath"].toString()));
|
||||||
|
}
|
||||||
|
for (const auto &im : mj["images"].toArray()) {
|
||||||
|
const QJsonObject io = im.toObject();
|
||||||
|
user.appendBlock(std::make_unique<StoredImageContent>(
|
||||||
|
io["fileName"].toString(),
|
||||||
|
io["storedPath"].toString(),
|
||||||
|
io["mediaType"].toString()));
|
||||||
|
}
|
||||||
|
history->append(std::move(user));
|
||||||
|
} else {
|
||||||
|
const QString content = mj["content"].toString();
|
||||||
|
if (content.trimmed().isEmpty())
|
||||||
|
continue;
|
||||||
|
const Message::Role mapped
|
||||||
|
= role == LegacyRole::System ? Message::Role::System : Message::Role::Assistant;
|
||||||
|
Message message(mapped, mj["id"].toString());
|
||||||
|
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
|
||||||
|
history->append(std::move(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -200,18 +244,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)
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -20,18 +22,14 @@ struct SerializationResult
|
|||||||
class ChatSerializer
|
class ChatSerializer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
|
static SerializationResult saveToFile(
|
||||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
const ConversationHistory *history, const QString &filePath);
|
||||||
|
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
|
||||||
// Public for testing purposes
|
|
||||||
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
|
|
||||||
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
|
|
||||||
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
|
||||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
|
||||||
|
|
||||||
// Content management (images and text files)
|
// 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);
|
||||||
@@ -39,7 +37,11 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString VERSION;
|
static const QString VERSION;
|
||||||
static constexpr int CURRENT_VERSION = 1;
|
|
||||||
|
static QJsonObject serializeChat(const ConversationHistory *history);
|
||||||
|
static SerializationResult loadCurrent(ConversationHistory *history, const QJsonObject &root);
|
||||||
|
static SerializationResult loadLegacy(ConversationHistory *history, const QJsonObject &root);
|
||||||
|
static void registerHistoricalFileEdits(const ConversationHistory *history);
|
||||||
|
|
||||||
static bool ensureDirectoryExists(const QString &filePath);
|
static bool ensureDirectoryExists(const QString &filePath);
|
||||||
static bool validateVersion(const QString &version);
|
static bool validateVersion(const QString &version);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "ChatUtils.h"
|
#include "ChatUtils.h"
|
||||||
|
|
||||||
@@ -19,22 +20,34 @@ QString ChatUtils::getSafeMarkdownText(const QString &text) const
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needsSanitization = false;
|
|
||||||
for (const QChar &ch : text) {
|
|
||||||
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
|
|
||||||
needsSanitization = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!needsSanitization) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString safeText;
|
QString safeText;
|
||||||
safeText.reserve(text.size());
|
safeText.reserve(text.size() + 16);
|
||||||
|
|
||||||
|
bool inFenced = false;
|
||||||
|
bool inInline = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < text.size(); ++i) {
|
||||||
|
const QChar ch = text[i];
|
||||||
|
|
||||||
|
if (!inInline && i + 2 < text.size()
|
||||||
|
&& text[i] == '`' && text[i + 1] == '`' && text[i + 2] == '`') {
|
||||||
|
safeText.append(QStringLiteral("```"));
|
||||||
|
inFenced = !inFenced;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inFenced && ch == '`') {
|
||||||
|
safeText.append(ch);
|
||||||
|
inInline = !inInline;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inFenced && !inInline && ch == '<') {
|
||||||
|
safeText.append(QStringLiteral("<"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (QChar ch : text) {
|
|
||||||
if (ch.isNull()) {
|
if (ch.isNull()) {
|
||||||
safeText.append(' ');
|
safeText.append(' ');
|
||||||
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
|
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "ChatView.hpp"
|
#include "ChatView.hpp"
|
||||||
|
|
||||||
|
#include <QQmlComponent>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
#include <QQuickItem>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
|
|
||||||
#include <coreplugin/actionmanager/actionmanager.h>
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
|
#include <coreplugin/actionmanager/command.h>
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
|
|
||||||
|
#include "ChatRootView.hpp"
|
||||||
|
#include "QodeAssistConstants.hpp"
|
||||||
|
#include "SessionFileRegistry.hpp"
|
||||||
|
#include "sources/skills/SkillsManager.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
|
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
|
||||||
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
|
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
|
||||||
@@ -19,30 +28,65 @@ constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::Win
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatView::ChatView()
|
ChatView::ChatView(
|
||||||
: m_isPin(false)
|
QQmlEngine *engine,
|
||||||
|
SessionFileRegistry *sessionFileRegistry,
|
||||||
|
Skills::SkillsManager *skillsManager)
|
||||||
|
: QQuickView{engine, nullptr}
|
||||||
|
, m_isPin(false)
|
||||||
{
|
{
|
||||||
setTitle("QodeAssist Chat");
|
setTitle("QodeAssist Chat");
|
||||||
engine()->rootContext()->setContextProperty("_chatview", this);
|
/// @note setup quick view content
|
||||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
{
|
||||||
|
auto context = new QQmlContext{engine, this};
|
||||||
|
context->setContextProperty("_chatview", this);
|
||||||
|
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
|
||||||
|
context->setContextProperty("skillsManager", skillsManager);
|
||||||
|
|
||||||
|
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
|
||||||
|
auto rootItem = component->create(context);
|
||||||
|
|
||||||
|
setContent(component->url(), component, rootItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto rootView = qobject_cast<ChatRootView *>(rootObject())) {
|
||||||
|
connect(
|
||||||
|
rootView,
|
||||||
|
&ChatRootView::closeHostRequested,
|
||||||
|
this,
|
||||||
|
&QWindow::close,
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
setResizeMode(QQuickView::SizeRootObjectToView);
|
setResizeMode(QQuickView::SizeRootObjectToView);
|
||||||
setMinimumSize({400, 300});
|
setMinimumSize({400, 300});
|
||||||
setFlags(baseFlags);
|
setFlags(baseFlags);
|
||||||
|
|
||||||
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
|
bindCommandShortcut("QodeAssist.CloseChatView", [this] { close(); });
|
||||||
m_closeShortcut = new QShortcut(action->keySequence(), this);
|
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE, [this] {
|
||||||
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
|
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
|
||||||
|
});
|
||||||
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
|
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_CLEAR_SESSION, [this] {
|
||||||
if (m_closeShortcut) {
|
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
||||||
m_closeShortcut->setKey(action->keySequence());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
restoreSettings();
|
restoreSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatView::bindCommandShortcut(Utils::Id commandId,
|
||||||
|
const std::function<void()> &onActivated)
|
||||||
|
{
|
||||||
|
auto command = Core::ActionManager::command(commandId);
|
||||||
|
if (!command)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto shortcut = new QShortcut(command->keySequence(), this);
|
||||||
|
connect(shortcut, &QShortcut::activated, this, onActivated);
|
||||||
|
connect(command, &Core::Command::keySequenceChanged, shortcut, [command, shortcut]() {
|
||||||
|
shortcut->setKey(command->keySequence());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void ChatView::closeEvent(QCloseEvent *event)
|
void ChatView::closeEvent(QCloseEvent *event)
|
||||||
{
|
{
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include <utils/id.h>
|
||||||
|
|
||||||
#include <QQuickView>
|
#include <QQuickView>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
|
|
||||||
|
namespace QodeAssist::Skills {
|
||||||
|
class SkillsManager;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class SessionFileRegistry;
|
||||||
|
|
||||||
class ChatView : public QQuickView
|
class ChatView : public QQuickView
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
|
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
|
||||||
public:
|
public:
|
||||||
ChatView();
|
ChatView(
|
||||||
|
QQmlEngine *engine,
|
||||||
|
SessionFileRegistry *sessionFileRegistry,
|
||||||
|
Skills::SkillsManager *skillsManager);
|
||||||
|
|
||||||
bool isPin() const;
|
bool isPin() const;
|
||||||
void setIsPin(bool newIsPin);
|
void setIsPin(bool newIsPin);
|
||||||
@@ -27,9 +41,9 @@ protected:
|
|||||||
private:
|
private:
|
||||||
void saveSettings();
|
void saveSettings();
|
||||||
void restoreSettings();
|
void restoreSettings();
|
||||||
|
void bindCommandShortcut(Utils::Id commandId, const std::function<void()> &onActivated);
|
||||||
|
|
||||||
bool m_isPin;
|
bool m_isPin;
|
||||||
QShortcut *m_closeShortcut;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,18 +1,68 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "ChatWidget.hpp"
|
#include "ChatWidget.hpp"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QMouseEvent>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
#include <QQuickItem>
|
||||||
|
|
||||||
|
#include <coreplugin/icontext.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include "QodeAssistConstants.hpp"
|
||||||
|
#include "SessionFileRegistry.hpp"
|
||||||
|
#include "sources/skills/SkillsManager.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatWidget::ChatWidget(QWidget *parent)
|
ChatWidget::ChatWidget(
|
||||||
: QQuickWidget(parent)
|
QQmlEngine *engine,
|
||||||
|
SessionFileRegistry *sessionFileRegistry,
|
||||||
|
Skills::SkillsManager *skillsManager,
|
||||||
|
bool registerOwnContext,
|
||||||
|
QWidget *parent)
|
||||||
|
: QQuickWidget{engine, parent}
|
||||||
{
|
{
|
||||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
/// @note setup quick view content
|
||||||
|
{
|
||||||
|
auto context = new QQmlContext{engine, this};
|
||||||
|
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
|
||||||
|
context->setContextProperty("skillsManager", skillsManager);
|
||||||
|
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
|
||||||
|
auto rootItem = component->create(context);
|
||||||
|
|
||||||
|
setContent(component->url(), component, rootItem);
|
||||||
|
}
|
||||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
|
||||||
|
setAttribute(Qt::WA_NoMousePropagation, true);
|
||||||
|
|
||||||
|
if (registerOwnContext) {
|
||||||
|
auto ideContext = new Core::IContext{this};
|
||||||
|
ideContext->setWidget(this);
|
||||||
|
ideContext->setContext(Core::Context{Constants::QODE_ASSIST_CHAT_CONTEXT});
|
||||||
|
Core::ICore::addContextObject(ideContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatWidget::focusInEvent(QFocusEvent *event)
|
||||||
|
{
|
||||||
|
QQuickWidget::focusInEvent(event);
|
||||||
|
if (rootObject())
|
||||||
|
QMetaObject::invokeMethod(rootObject(), "focusInput");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatWidget::mousePressEvent(QMouseEvent *event)
|
||||||
|
{
|
||||||
|
if (!hasFocus())
|
||||||
|
setFocus(Qt::MouseFocusReason);
|
||||||
|
|
||||||
|
QQuickWidget::mousePressEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatWidget::clear()
|
void ChatWidget::clear()
|
||||||
@@ -24,4 +74,35 @@ void ChatWidget::scrollToBottom()
|
|||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatWidget::focusInput()
|
||||||
|
{
|
||||||
|
setFocus(Qt::OtherFocusReason);
|
||||||
|
QMetaObject::invokeMethod(rootObject(), "focusInput");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatWidget::isChatFocused() const
|
||||||
|
{
|
||||||
|
return hasFocus() || (rootObject() && rootObject()->hasActiveFocus());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatWidget::sendMessage()
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatWidget::clearSession()
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatWidget *ChatWidget::focusedInstance()
|
||||||
|
{
|
||||||
|
for (QWidget *widget = QApplication::focusWidget(); widget;
|
||||||
|
widget = widget->parentWidget()) {
|
||||||
|
if (auto chatWidget = qobject_cast<ChatWidget *>(widget))
|
||||||
|
return chatWidget;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,25 +1,49 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QtQuickWidgets/QtQuickWidgets>
|
#include <QtQuickWidgets/QtQuickWidgets>
|
||||||
|
|
||||||
|
namespace QodeAssist::Skills {
|
||||||
|
class SkillsManager;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class SessionFileRegistry;
|
||||||
|
|
||||||
class ChatWidget : public QQuickWidget
|
class ChatWidget : public QQuickWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatWidget(QWidget *parent = nullptr);
|
explicit ChatWidget(
|
||||||
|
QQmlEngine *engine,
|
||||||
|
SessionFileRegistry *sessionFileRegistry,
|
||||||
|
Skills::SkillsManager *skillsManager,
|
||||||
|
bool registerOwnContext = true,
|
||||||
|
QWidget *parent = nullptr);
|
||||||
~ChatWidget() = default;
|
~ChatWidget() = default;
|
||||||
|
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
Q_INVOKABLE void scrollToBottom();
|
Q_INVOKABLE void scrollToBottom();
|
||||||
|
Q_INVOKABLE void focusInput();
|
||||||
|
|
||||||
|
void sendMessage();
|
||||||
|
void clearSession();
|
||||||
|
|
||||||
|
bool isChatFocused() const;
|
||||||
|
|
||||||
|
static ChatWidget *focusedInstance();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void clearPressed();
|
void clearPressed();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void focusInEvent(QFocusEvent *event) override;
|
||||||
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,51 +1,70 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <projectexplorer/buildconfiguration.h>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <projectexplorer/target.h>
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
#include <texteditor/textdocument.h>
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
#include <QFile>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QImageReader>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QMimeDatabase>
|
|
||||||
#include <QUuid>
|
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <coreplugin/editormanager/ieditor.h>
|
#include <coreplugin/editormanager/ieditor.h>
|
||||||
#include <coreplugin/idocument.h>
|
#include <coreplugin/idocument.h>
|
||||||
|
#include <projectexplorer/buildconfiguration.h>
|
||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectexplorer.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <projectexplorer/target.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <ContextRenderer.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
#include <PluginBlocks.hpp>
|
||||||
|
#include <Session.hpp>
|
||||||
|
#include <SessionManager.hpp>
|
||||||
|
#include <SystemPromptBuilder.hpp>
|
||||||
|
|
||||||
|
#include "tools/ReadOriginalHistoryTool.hpp"
|
||||||
#include "tools/TodoTool.hpp"
|
#include "tools/TodoTool.hpp"
|
||||||
|
#include "tools/ToolsRegistration.hpp"
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
|
#include "SkillsSettings.hpp"
|
||||||
#include "ToolsSettings.hpp"
|
#include "ToolsSettings.hpp"
|
||||||
#include <RulesLoader.hpp>
|
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
|
#include <sources/skills/SkillsManager.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ClientInterface::ClientInterface(
|
namespace {
|
||||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
struct StoredImage
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString storedPath;
|
||||||
|
QString mediaType;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
, m_promptProvider(promptProvider)
|
|
||||||
, m_contextManager(new Context::ContextManager(this))
|
, m_contextManager(new Context::ContextManager(this))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
@@ -54,28 +73,53 @@ ClientInterface::~ClientInterface()
|
|||||||
cancelRequest();
|
cancelRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
|
||||||
|
{
|
||||||
|
m_skillsManager = skillsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setHistory(ConversationHistory *history)
|
||||||
|
{
|
||||||
|
m_history = history;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setActiveAgent(const QString &agentName)
|
||||||
|
{
|
||||||
|
m_activeAgent = agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setActiveRole(const QString &roleId)
|
||||||
|
{
|
||||||
|
m_activeRoleId = roleId;
|
||||||
|
}
|
||||||
|
|
||||||
void ClientInterface::sendMessage(
|
void ClientInterface::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,
|
|
||||||
bool useThinking)
|
|
||||||
{
|
{
|
||||||
|
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
||||||
|
LOG_MESSAGE("Ignoring empty chat message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
|
|
||||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||||
|
|
||||||
QList<QString> imageFiles;
|
QList<QString> imageFiles;
|
||||||
QList<QString> textFiles;
|
QList<QString> textFiles;
|
||||||
|
|
||||||
for (const QString &filePath : attachments) {
|
for (const QString &filePath : attachments) {
|
||||||
if (isImageFile(filePath)) {
|
if (isImageFile(filePath))
|
||||||
imageFiles.append(filePath);
|
imageFiles.append(filePath);
|
||||||
} else {
|
else
|
||||||
textFiles.append(filePath);
|
textFiles.append(filePath);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
QList<Context::ContentFile> storedAttachments;
|
QList<Context::ContentFile> storedAttachments;
|
||||||
if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
@@ -96,24 +140,19 @@ void ClientInterface::sendMessage(
|
|||||||
.arg(textFiles.size()));
|
.arg(textFiles.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ChatModel::ImageAttachment> imageAttachments;
|
QList<StoredImage> storedImages;
|
||||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
for (const QString &imagePath : imageFiles) {
|
for (const QString &imagePath : imageFiles) {
|
||||||
QString base64Data = encodeImageToBase64(imagePath);
|
QString base64Data = encodeImageToBase64(imagePath);
|
||||||
if (base64Data.isEmpty()) {
|
if (base64Data.isEmpty())
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
QString storedPath;
|
QString storedPath;
|
||||||
QFileInfo fileInfo(imagePath);
|
QFileInfo fileInfo(imagePath);
|
||||||
if (ChatSerializer::saveContentToStorage(
|
if (ChatSerializer::saveContentToStorage(
|
||||||
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
||||||
ChatModel::ImageAttachment imageAttachment;
|
storedImages.append(
|
||||||
imageAttachment.fileName = fileInfo.fileName();
|
{fileInfo.fileName(), storedPath, getMediaTypeForImage(imagePath)});
|
||||||
imageAttachment.storedPath = storedPath;
|
|
||||||
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
|
||||||
imageAttachments.append(imageAttachment);
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,238 +161,287 @@ void ClientInterface::sendMessage(
|
|||||||
.arg(imageFiles.size()));
|
.arg(imageFiles.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
if (!m_sessionManager) {
|
||||||
|
const QString error = QStringLiteral("Chat session manager is not available");
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
return;
|
||||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
}
|
||||||
|
if (!m_history) {
|
||||||
if (!provider) {
|
const QString error = QStringLiteral("Chat history is not available");
|
||||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().caTemplate();
|
QString sessionError;
|
||||||
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError);
|
||||||
|
if (!session) {
|
||||||
if (!promptTemplate) {
|
const QString error = sessionError.isEmpty()
|
||||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
? QStringLiteral("No chat agent selected")
|
||||||
|
: sessionError;
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit errorOccurred(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData context;
|
auto *client = session->client();
|
||||||
|
if (!client) {
|
||||||
const bool isToolsEnabled = useTools;
|
const QString error = QStringLiteral("Chat agent has no live client");
|
||||||
|
LOG_MESSAGE(error);
|
||||||
if (chatAssistantSettings.useSystemPrompt()) {
|
m_sessionManager->removeSession(session);
|
||||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
emit errorOccurred(error);
|
||||||
|
return;
|
||||||
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
|
|
||||||
if (!lastRoleId.isEmpty()) {
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (!role.id.isEmpty())
|
|
||||||
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
|
Templates::ContextRenderer::Bindings bindings;
|
||||||
|
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
|
||||||
|
bindings.homeDir = QDir::homePath();
|
||||||
|
bindings.roleId = m_activeRoleId;
|
||||||
|
session->setContextBindings(bindings);
|
||||||
|
|
||||||
|
const QString chatFilePath = m_chatFilePath;
|
||||||
|
session->setContentLoader([chatFilePath](const QString &storedPath) {
|
||||||
|
return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_sessionManager->toolContributors().contribute(client->tools());
|
||||||
|
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||||
|
client->setTransferTimeout(
|
||||||
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
|
|
||||||
|
const QString chatContext = buildChatContextLayer(message, linkedFiles);
|
||||||
|
if (!chatContext.isEmpty())
|
||||||
|
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(message));
|
||||||
|
|
||||||
|
for (const auto &attachment : storedAttachments) {
|
||||||
|
blocks.push_back(
|
||||||
|
std::make_unique<StoredAttachmentContent>(attachment.filename, attachment.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!storedImages.isEmpty() && session->supportsImages()) {
|
||||||
|
for (const auto &image : storedImages) {
|
||||||
|
blocks.push_back(std::make_unique<StoredImageContent>(
|
||||||
|
image.fileName, image.storedPath, image.mediaType));
|
||||||
|
}
|
||||||
|
} else if (!storedImages.isEmpty() && !session->supportsImages()) {
|
||||||
|
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
|
||||||
|
.arg(m_activeAgent)
|
||||||
|
.arg(storedImages.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
|
if (auto *todoTool
|
||||||
|
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
|
||||||
|
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||||
|
}
|
||||||
|
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
|
||||||
|
client->tools()->tool("read_original_history"))) {
|
||||||
|
historyTool->setCurrentSessionId(m_chatFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(session, &Session::event, this, [this, session](const QodeAssist::ResponseEvent &ev) {
|
||||||
|
onSessionEvent(session, ev);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
session, &Session::finished, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QString &) { onSessionFinished(id); });
|
||||||
|
connect(
|
||||||
|
session, &Session::failed, this,
|
||||||
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
|
onSessionFailed(id, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
const LLMQore::RequestID requestId = session->send(std::move(blocks));
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
|
||||||
|
.arg(m_activeAgent, session->lastError().message);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emit errorOccurred(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
||||||
|
|
||||||
|
emit requestStarted(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::requestIdForSession(Session *session) const
|
||||||
|
{
|
||||||
|
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
|
||||||
|
if (it.value().session == session)
|
||||||
|
return it.key();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev)
|
||||||
|
{
|
||||||
|
if (ev.kind() != ResponseEvent::Kind::Usage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto *usage = ev.as<ResponseEvents::Usage>();
|
||||||
|
if (!usage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString requestId = requestIdForSession(session);
|
||||||
|
if (!requestId.isEmpty()) {
|
||||||
|
m_chatModel->setMessageUsage(
|
||||||
|
requestId,
|
||||||
|
usage->inputTokens,
|
||||||
|
usage->outputTokens,
|
||||||
|
usage->cachedTokens,
|
||||||
|
usage->reasoningTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit messageUsageReceived(
|
||||||
|
usage->inputTokens, usage->outputTokens, usage->cachedTokens, usage->reasoningTokens);
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||||
|
.arg(requestId)
|
||||||
|
.arg(usage->inputTokens)
|
||||||
|
.arg(usage->outputTokens)
|
||||||
|
.arg(usage->cachedTokens)
|
||||||
|
.arg(usage->reasoningTokens));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionFinished(const QString &requestId)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
|
|
||||||
|
QString applyError;
|
||||||
|
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
|
||||||
|
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||||
|
.arg(requestId, applyError));
|
||||||
|
}
|
||||||
|
|
||||||
|
emit messageReceivedCompletely();
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Session *session = it.value().session;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
|
||||||
|
emit errorOccurred(error.message);
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::buildChatContextLayer(
|
||||||
|
const QString &message, const QList<QString> &linkedFiles) const
|
||||||
|
{
|
||||||
|
QString context;
|
||||||
|
|
||||||
|
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||||
if (project) {
|
if (project) {
|
||||||
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
context += QString("# Active project: %1").arg(project->displayName());
|
||||||
systemPrompt += QString("\n# Active Project path: %1")
|
context += QString(
|
||||||
|
"\n# Project source root: %1"
|
||||||
|
"\n# All new source files, headers, QML and CMake edits MUST be "
|
||||||
|
"created or modified under this directory. Use absolute paths "
|
||||||
|
"rooted here, or project-relative paths.")
|
||||||
.arg(project->projectDirectory().toUrlishString());
|
.arg(project->projectDirectory().toUrlishString());
|
||||||
|
|
||||||
if (auto target = project->activeTarget()) {
|
if (auto target = project->activeTarget()) {
|
||||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||||
systemPrompt += QString("\n# Active Build directory: %1")
|
context += QString(
|
||||||
|
"\n# Build output directory (compiler artifacts only — do NOT "
|
||||||
|
"create or edit source files here): %1")
|
||||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString projectRules
|
|
||||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
systemPrompt += QString("\n# No active project in IDE");
|
context += QString("# No active project in IDE");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
||||||
|
QStringList projectSkillDirs;
|
||||||
|
if (project) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
|
projectSkillDirs
|
||||||
|
= Settings::SkillsSettings::splitLines(projectSettings.projectSkillDirs());
|
||||||
|
}
|
||||||
|
m_skillsManager->configure(
|
||||||
|
project ? project->projectDirectory().toFSPathString() : QString(),
|
||||||
|
Settings::SkillsSettings::splitPaths(Settings::skillsSettings().globalSkillRoots()),
|
||||||
|
projectSkillDirs);
|
||||||
|
|
||||||
|
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
||||||
|
if (!alwaysOnSkills.isEmpty())
|
||||||
|
context += QString("\n\n") + alwaysOnSkills;
|
||||||
|
|
||||||
|
const QString skillsCatalog = m_skillsManager->catalogText();
|
||||||
|
if (!skillsCatalog.isEmpty())
|
||||||
|
context += QString("\n\n") + skillsCatalog;
|
||||||
|
|
||||||
|
static const QRegularExpression skillCommand(
|
||||||
|
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
||||||
|
QStringList invokedSkillNames;
|
||||||
|
auto skillMatch = skillCommand.globalMatch(message);
|
||||||
|
while (skillMatch.hasNext()) {
|
||||||
|
const QString skillName = skillMatch.next().captured(1);
|
||||||
|
if (invokedSkillNames.contains(skillName))
|
||||||
|
continue;
|
||||||
|
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
||||||
|
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
||||||
|
invokedSkillNames << skillName;
|
||||||
|
context += QString("\n\n# Invoked Skill: %1\n\n%2")
|
||||||
|
.arg(invokedSkill->name, invokedSkill->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
if (!linkedFiles.isEmpty()) {
|
||||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
context += "\n\nLinked files for reference:\n";
|
||||||
}
|
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
||||||
context.systemPrompt = systemPrompt;
|
for (const auto &file : contentFiles)
|
||||||
|
context += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
return context;
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
|
||||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::Message apiMessage;
|
|
||||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
|
||||||
apiMessage.content = msg.content;
|
|
||||||
|
|
||||||
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
|
||||||
apiMessage.content += "\n\nAttached files:";
|
|
||||||
for (const auto &attachment : msg.attachments) {
|
|
||||||
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
|
||||||
if (!fileContent.isEmpty()) {
|
|
||||||
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
|
||||||
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
|
|
||||||
.arg(attachment.filename, decodedContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
|
||||||
apiMessage.isRedacted = msg.isRedacted;
|
|
||||||
apiMessage.signature = msg.signature;
|
|
||||||
|
|
||||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
|
|
||||||
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
|
||||||
auto apiImages = loadImagesFromStorage(msg.images);
|
|
||||||
if (!apiImages.isEmpty()) {
|
|
||||||
apiMessage.images = apiImages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.append(apiMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageFiles.isEmpty()
|
|
||||||
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
|
|
||||||
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
|
||||||
.arg(provider->name(), QString::number(imageFiles.size())));
|
|
||||||
}
|
|
||||||
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
QJsonObject payload{
|
|
||||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
|
||||||
|
|
||||||
provider->prepareRequest(
|
|
||||||
payload,
|
|
||||||
promptTemplate,
|
|
||||||
context,
|
|
||||||
PluginLLMCore::RequestType::Chat,
|
|
||||||
useTools,
|
|
||||||
useThinking);
|
|
||||||
|
|
||||||
provider->client()->setMaxToolContinuations(
|
|
||||||
Settings::toolsSettings().maxToolContinuations());
|
|
||||||
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::chunkReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handlePartialResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::toolStarted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleToolExecutionStarted,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::toolResultReady,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleToolExecutionCompleted,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::thinkingBlockReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleThinkingBlockReceived,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
|
||||||
: promptTemplate->endpoint();
|
|
||||||
auto requestId
|
|
||||||
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
|
||||||
|
|
||||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
|
||||||
&& provider->toolsManager()) {
|
|
||||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
|
||||||
provider->toolsManager()->tool("todo_tool"))) {
|
|
||||||
todoTool->setCurrentSessionId(m_chatFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::clearMessages()
|
void ClientInterface::clearMessages()
|
||||||
{
|
{
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
if (m_history)
|
||||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
m_history->clear();
|
||||||
|
|
||||||
if (provider && !m_chatFilePath.isEmpty()
|
|
||||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
|
||||||
&& provider->toolsManager()) {
|
|
||||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
|
||||||
provider->toolsManager()->tool("todo_tool"))) {
|
|
||||||
todoTool->clearSession(m_chatFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatModel->clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
{
|
{
|
||||||
QSet<PluginLLMCore::Provider *> providers;
|
const auto requests = m_activeRequests;
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.value().provider) {
|
|
||||||
providers.insert(it.value().provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
|
||||||
disconnect(provider->client(), nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
if (ctx.provider) {
|
|
||||||
ctx.provider->cancelRequest(it.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
m_accumulatedResponses.clear();
|
|
||||||
m_awaitingContinuation.clear();
|
|
||||||
|
|
||||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
for (auto it = requests.begin(); it != requests.end(); ++it) {
|
||||||
|
Session *session = it.value().session;
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
|
LOG_MESSAGE("All chat requests cancelled and state cleared");
|
||||||
{
|
|
||||||
const auto message = response.trimmed();
|
|
||||||
|
|
||||||
if (!message.isEmpty()) {
|
|
||||||
QString messageId = request["id"].toString();
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ClientInterface::getCurrentFileContext() const
|
QString ClientInterface::getCurrentFileContext() const
|
||||||
@@ -380,136 +468,11 @@ QString ClientInterface::getCurrentFileContext() const
|
|||||||
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ClientInterface::getSystemPromptWithLinkedFiles(
|
|
||||||
const QString &basePrompt, const QList<QString> &linkedFiles) const
|
|
||||||
{
|
|
||||||
QString updatedPrompt = basePrompt;
|
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
|
||||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
|
||||||
|
|
||||||
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
|
|
||||||
for (const auto &file : contentFiles) {
|
|
||||||
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
Context::ContextManager *ClientInterface::contextManager() const
|
Context::ContextManager *ClientInterface::contextManager() const
|
||||||
{
|
{
|
||||||
return m_contextManager;
|
return m_contextManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (m_awaitingContinuation.remove(requestId)) {
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_accumulatedResponses[requestId] += partialText;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
|
|
||||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
|
||||||
|
|
||||||
QString applyError;
|
|
||||||
bool applySuccess
|
|
||||||
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
|
|
||||||
|
|
||||||
if (!applySuccess) {
|
|
||||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
|
||||||
.arg(requestId, applyError));
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
|
||||||
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
|
|
||||||
+ ": " + finalText);
|
|
||||||
emit messageReceivedCompletely();
|
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
m_awaitingContinuation.remove(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
auto it = m_activeRequests.find(requestId);
|
|
||||||
if (it == m_activeRequests.end())
|
|
||||||
return;
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
|
||||||
emit errorOccurred(error);
|
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
m_awaitingContinuation.remove(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleThinkingBlockReceived(
|
|
||||||
const QString &requestId, const QString &thinking, const QString &signature)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_awaitingContinuation.remove(requestId)) {
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thinking.isEmpty()) {
|
|
||||||
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
|
||||||
} else {
|
|
||||||
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionStarted(
|
|
||||||
const QString &requestId, const QString &toolId, const QString &toolName)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
|
||||||
m_awaitingContinuation.insert(requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionCompleted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &toolOutput)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ClientInterface::isImageFile(const QString &filePath) const
|
bool ClientInterface::isImageFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||||
@@ -557,46 +520,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
|||||||
return imageData.toBase64();
|
return imageData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
|
||||||
const QList<ChatModel::ImageAttachment> &storedImages) const
|
|
||||||
{
|
|
||||||
QVector<PluginLLMCore::ImageAttachment> apiImages;
|
|
||||||
|
|
||||||
for (const auto &storedImage : storedImages) {
|
|
||||||
QString base64Data
|
|
||||||
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
|
||||||
if (base64Data.isEmpty()) {
|
|
||||||
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::ImageAttachment apiImage;
|
|
||||||
apiImage.data = base64Data;
|
|
||||||
apiImage.mediaType = storedImage.mediaType;
|
|
||||||
apiImage.isUrl = false;
|
|
||||||
|
|
||||||
apiImages.append(apiImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiImages;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::setChatFilePath(const QString &filePath)
|
void ClientInterface::setChatFilePath(const QString &filePath)
|
||||||
{
|
{
|
||||||
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
|
|
||||||
if (provider
|
|
||||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
|
||||||
&& provider->toolsManager()) {
|
|
||||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
|
||||||
provider->toolsManager()->tool("todo_tool"))) {
|
|
||||||
todoTool->clearSession(m_chatFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_chatFilePath = filePath;
|
m_chatFilePath = filePath;
|
||||||
m_chatModel->setChatFilePath(filePath);
|
m_chatModel->setChatFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSet>
|
#include <QPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "Provider.hpp"
|
#include <ErrorInfo.hpp>
|
||||||
#include "pluginllmcore/IPromptProvider.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::Skills {
|
||||||
|
class SkillsManager;
|
||||||
|
}
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ClientInterface : public QObject
|
class ClientInterface : public QObject
|
||||||
@@ -20,16 +31,19 @@ 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 setSessionManager(SessionManager *sessionManager);
|
||||||
|
void setHistory(ConversationHistory *history);
|
||||||
|
void setActiveAgent(const QString &agentName);
|
||||||
|
void setActiveRole(const QString &roleId);
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@@ -42,45 +56,38 @@ 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 messageUsageReceived(
|
||||||
private slots:
|
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
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);
|
|
||||||
void handleToolExecutionCompleted(
|
|
||||||
const QString &requestId,
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QString &toolOutput);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
|
||||||
|
void onSessionFinished(const QString &requestId);
|
||||||
|
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
|
||||||
|
|
||||||
QString getCurrentFileContext() const;
|
QString getCurrentFileContext() const;
|
||||||
QString getSystemPromptWithLinkedFiles(
|
QString buildChatContextLayer(
|
||||||
const QString &basePrompt, const QList<QString> &linkedFiles) const;
|
const QString &message, const QList<QString> &linkedFiles) 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
QPointer<SessionManager> m_sessionManager;
|
||||||
|
QString m_activeAgent;
|
||||||
|
QString m_activeRoleId;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
278
ChatView/FileEditController.cpp
Normal file
278
ChatView/FileEditController.cpp
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include "FileEditController.hpp"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
FileEditController::FileEditController(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
auto &changes = Context::ChangesManager::instance();
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditApplied, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditRejected, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditUndone, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditArchived, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::setCurrentRequestId(const QString &requestId)
|
||||||
|
{
|
||||||
|
if (!m_currentRequestId.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentRequestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentRequestId = requestId;
|
||||||
|
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::clearCurrentRequestId()
|
||||||
|
{
|
||||||
|
m_currentRequestId.clear();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::totalEdits() const
|
||||||
|
{
|
||||||
|
return m_totalEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::appliedEdits() const
|
||||||
|
{
|
||||||
|
return m_appliedEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::pendingEdits() const
|
||||||
|
{
|
||||||
|
return m_pendingEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::rejectedEdits() const
|
||||||
|
{
|
||||||
|
return m_rejectedEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::applyFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||||
|
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||||
|
emit infoMessage(QString("File edit applied successfully"));
|
||||||
|
} else {
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
emit errorOccurred(
|
||||||
|
edit.statusMessage.isEmpty()
|
||||||
|
? QString("Failed to apply file edit")
|
||||||
|
: QString("Failed to apply file edit: %1").arg(edit.statusMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::rejectFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||||
|
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||||
|
emit infoMessage(QString("File edit rejected"));
|
||||||
|
} else {
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
emit errorOccurred(
|
||||||
|
edit.statusMessage.isEmpty()
|
||||||
|
? QString("Failed to reject file edit")
|
||||||
|
: QString("Failed to reject file edit: %1").arg(edit.statusMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::undoFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||||
|
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||||
|
emit infoMessage(QString("File edit undone successfully"));
|
||||||
|
} else {
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
emit errorOccurred(
|
||||||
|
edit.statusMessage.isEmpty()
|
||||||
|
? QString("Failed to undo file edit")
|
||||||
|
: QString("Failed to undo file edit: %1").arg(edit.statusMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::openFileEditInEditor(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
|
||||||
|
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
if (edit.editId.isEmpty()) {
|
||||||
|
emit errorOccurred(QString("File edit not found: %1").arg(editId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
|
||||||
|
|
||||||
|
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
|
||||||
|
if (!editor) {
|
||||||
|
emit errorOccurred(QString("Failed to open file in editor: %1").arg(edit.filePath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
|
||||||
|
if (textEditor && textEditor->editorWidget()) {
|
||||||
|
QTextDocument *doc = textEditor->editorWidget()->document();
|
||||||
|
if (doc) {
|
||||||
|
QString currentContent = doc->toPlainText();
|
||||||
|
int position = -1;
|
||||||
|
|
||||||
|
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
||||||
|
position = currentContent.indexOf(edit.newContent);
|
||||||
|
} else if (!edit.oldContent.isEmpty()) {
|
||||||
|
position = currentContent.indexOf(edit.oldContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position >= 0) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
textEditor->editorWidget()->setTextCursor(cursor);
|
||||||
|
textEditor->editorWidget()->centerCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::applyAllForCurrentMessage()
|
||||||
|
{
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
emit errorOccurred(QString("No active message with file edits"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentRequestId));
|
||||||
|
|
||||||
|
QString errorMsg;
|
||||||
|
bool success = Context::ChangesManager::instance()
|
||||||
|
.reapplyAllEditsForRequest(m_currentRequestId, &errorMsg);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
emit infoMessage(QString("All file edits applied successfully"));
|
||||||
|
} else {
|
||||||
|
emit errorOccurred(
|
||||||
|
errorMsg.isEmpty()
|
||||||
|
? QString("Failed to apply some file edits")
|
||||||
|
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::undoAllForCurrentMessage()
|
||||||
|
{
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
emit errorOccurred(QString("No active message with file edits"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentRequestId));
|
||||||
|
|
||||||
|
QString errorMsg;
|
||||||
|
bool success = Context::ChangesManager::instance()
|
||||||
|
.undoAllEditsForRequest(m_currentRequestId, &errorMsg);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
emit infoMessage(QString("All file edits undone successfully"));
|
||||||
|
} else {
|
||||||
|
emit errorOccurred(
|
||||||
|
errorMsg.isEmpty()
|
||||||
|
? QString("Failed to undo some file edits")
|
||||||
|
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::updateStats()
|
||||||
|
{
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
if (m_totalEdits != 0 || m_appliedEdits != 0 || m_pendingEdits != 0
|
||||||
|
|| m_rejectedEdits != 0) {
|
||||||
|
m_totalEdits = 0;
|
||||||
|
m_appliedEdits = 0;
|
||||||
|
m_pendingEdits = 0;
|
||||||
|
m_rejectedEdits = 0;
|
||||||
|
emit statsChanged();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||||
|
|
||||||
|
int total = edits.size();
|
||||||
|
int applied = 0;
|
||||||
|
int pending = 0;
|
||||||
|
int rejected = 0;
|
||||||
|
|
||||||
|
for (const auto &edit : edits) {
|
||||||
|
switch (edit.status) {
|
||||||
|
case Context::ChangesManager::Applied:
|
||||||
|
applied++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Pending:
|
||||||
|
pending++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Rejected:
|
||||||
|
rejected++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Archived:
|
||||||
|
total--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (m_totalEdits != total) {
|
||||||
|
m_totalEdits = total;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_appliedEdits != applied) {
|
||||||
|
m_appliedEdits = applied;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_pendingEdits != pending) {
|
||||||
|
m_pendingEdits = pending;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_rejectedEdits != rejected) {
|
||||||
|
m_rejectedEdits = rejected;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
||||||
|
.arg(total)
|
||||||
|
.arg(applied)
|
||||||
|
.arg(pending)
|
||||||
|
.arg(rejected));
|
||||||
|
emit statsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
49
ChatView/FileEditController.hpp
Normal file
49
ChatView/FileEditController.hpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 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 <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class FileEditController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FileEditController(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setCurrentRequestId(const QString &requestId);
|
||||||
|
void clearCurrentRequestId();
|
||||||
|
|
||||||
|
int totalEdits() const;
|
||||||
|
int appliedEdits() const;
|
||||||
|
int pendingEdits() const;
|
||||||
|
int rejectedEdits() const;
|
||||||
|
|
||||||
|
void applyFileEdit(const QString &editId);
|
||||||
|
void rejectFileEdit(const QString &editId);
|
||||||
|
void undoFileEdit(const QString &editId);
|
||||||
|
void openFileEditInEditor(const QString &editId);
|
||||||
|
|
||||||
|
void applyAllForCurrentMessage();
|
||||||
|
void undoAllForCurrentMessage();
|
||||||
|
void updateStats();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void statsChanged();
|
||||||
|
void infoMessage(const QString &message);
|
||||||
|
void errorOccurred(const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_currentRequestId;
|
||||||
|
int m_totalEdits{0};
|
||||||
|
int m_appliedEdits{0};
|
||||||
|
int m_pendingEdits{0};
|
||||||
|
int m_rejectedEdits{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "FileItem.hpp"
|
#include "FileItem.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2026 Petr Mironychev
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "FileMentionItem.hpp"
|
#include "FileMentionItem.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2026 Petr Mironychev
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
142
ChatView/InputTokenCounter.cpp
Normal file
142
ChatView/InputTokenCounter.cpp
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include "InputTokenCounter.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "context/ContextManager.hpp"
|
||||||
|
#include "context/TokenUtils.hpp"
|
||||||
|
|
||||||
|
#include <ConversationHistory.hpp>
|
||||||
|
#include <Message.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
InputTokenCounter::InputTokenCounter(
|
||||||
|
ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_history(history)
|
||||||
|
, m_contextManager(contextManager)
|
||||||
|
{
|
||||||
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
connect(
|
||||||
|
&settings.useSystemPrompt,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&InputTokenCounter::recompute);
|
||||||
|
connect(
|
||||||
|
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
|
||||||
|
connect(
|
||||||
|
&settings.enableChatTools,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&InputTokenCounter::recompute);
|
||||||
|
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
int InputTokenCounter::inputTokens() const
|
||||||
|
{
|
||||||
|
return m_inputTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::setMessage(const QString &message)
|
||||||
|
{
|
||||||
|
m_messageTokens = Context::TokenUtils::estimateTokens(message);
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::setAttachments(const QStringList &attachments)
|
||||||
|
{
|
||||||
|
m_attachments = attachments;
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
||||||
|
{
|
||||||
|
m_linkedFiles = linkedFiles;
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::recompute()
|
||||||
|
{
|
||||||
|
int inputTokens = m_messageTokens;
|
||||||
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
|
if (settings.useSystemPrompt()) {
|
||||||
|
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
|
||||||
|
int imageTokens = 0;
|
||||||
|
for (const QString &p : paths) {
|
||||||
|
if (Context::TokenUtils::isImageFilePath(p))
|
||||||
|
imageTokens += Context::TokenUtils::estimateImageAttachmentTokens(p);
|
||||||
|
else
|
||||||
|
textPaths.append(p);
|
||||||
|
}
|
||||||
|
return imageTokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!m_attachments.isEmpty()) {
|
||||||
|
QStringList textPaths;
|
||||||
|
inputTokens += splitImageEstimate(m_attachments, textPaths);
|
||||||
|
if (!textPaths.isEmpty()) {
|
||||||
|
auto attachFiles = m_contextManager->getContentFiles(textPaths);
|
||||||
|
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_linkedFiles.isEmpty()) {
|
||||||
|
QStringList textPaths;
|
||||||
|
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
|
||||||
|
if (!textPaths.isEmpty()) {
|
||||||
|
auto linkFiles = m_contextManager->getContentFiles(textPaths);
|
||||||
|
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_history) {
|
||||||
|
for (const auto &message : m_history->messages()) {
|
||||||
|
inputTokens += Context::TokenUtils::estimateTokens(message.text());
|
||||||
|
inputTokens += 4; // + role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
||||||
|
emit inputTokensChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::recordSent()
|
||||||
|
{
|
||||||
|
m_lastSentEstimate = m_calibrationFactor > 0.0
|
||||||
|
? static_cast<int>(m_inputTokens / m_calibrationFactor)
|
||||||
|
: m_inputTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::recordServerUsage(int promptTokens)
|
||||||
|
{
|
||||||
|
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const double rawFactor
|
||||||
|
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
|
||||||
|
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
|
||||||
|
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
|
||||||
|
.arg(promptTokens)
|
||||||
|
.arg(m_lastSentEstimate)
|
||||||
|
.arg(rawFactor, 0, 'f', 3)
|
||||||
|
.arg(m_calibrationFactor, 0, 'f', 3));
|
||||||
|
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
55
ChatView/InputTokenCounter.hpp
Normal file
55
ChatView/InputTokenCounter.hpp
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
class ConversationHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
class ContextManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class InputTokenCounter : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
InputTokenCounter(
|
||||||
|
ConversationHistory *history,
|
||||||
|
Context::ContextManager *contextManager,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int inputTokens() const;
|
||||||
|
|
||||||
|
void setMessage(const QString &message);
|
||||||
|
void setAttachments(const QStringList &attachments);
|
||||||
|
void setLinkedFiles(const QStringList &linkedFiles);
|
||||||
|
void recompute();
|
||||||
|
|
||||||
|
void recordSent();
|
||||||
|
void recordServerUsage(int promptTokens);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void inputTokensChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ConversationHistory *m_history;
|
||||||
|
Context::ContextManager *m_contextManager;
|
||||||
|
|
||||||
|
QStringList m_attachments;
|
||||||
|
QStringList m_linkedFiles;
|
||||||
|
int m_messageTokens{0};
|
||||||
|
int m_inputTokens{0};
|
||||||
|
int m_lastSentEstimate{0};
|
||||||
|
double m_calibrationFactor{1.0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
68
ChatView/SessionFileRegistry.cpp
Normal file
68
ChatView/SessionFileRegistry.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#include "SessionFileRegistry.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool SessionFileRegistry::isLocked(const QString &path) const
|
||||||
|
{
|
||||||
|
return !path.isEmpty() && m_lockedPaths.contains(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SessionFileRegistry::lock(const QString &path)
|
||||||
|
{
|
||||||
|
if (path.isEmpty() || m_lockedPaths.contains(path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_lockedPaths.insert(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionFileRegistry::release(const QString &path)
|
||||||
|
{
|
||||||
|
m_lockedPaths.remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
||||||
|
{
|
||||||
|
m_pendingChatFile = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SessionFileRegistry::takePendingChatFile()
|
||||||
|
{
|
||||||
|
return std::exchange(m_pendingChatFile, QString{});
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
||||||
|
{
|
||||||
|
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
|
||||||
|
return desiredPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfo info(desiredPath);
|
||||||
|
const QString dir = info.path();
|
||||||
|
const QString base = info.completeBaseName();
|
||||||
|
const QString suffix = info.suffix();
|
||||||
|
|
||||||
|
for (int counter = 2;; ++counter) {
|
||||||
|
QString candidate = dir + '/' + base + '_' + QString::number(counter);
|
||||||
|
if (!suffix.isEmpty()) {
|
||||||
|
candidate += '.' + suffix;
|
||||||
|
}
|
||||||
|
if (!m_lockedPaths.contains(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
39
ChatView/SessionFileRegistry.hpp
Normal file
39
ChatView/SessionFileRegistry.hpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SessionFileRegistry(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
bool isLocked(const QString &path) const;
|
||||||
|
bool lock(const QString &path);
|
||||||
|
void release(const QString &path);
|
||||||
|
|
||||||
|
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);
|
||||||
|
QString takePendingChatFile();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QSet<QString> m_lockedPaths;
|
||||||
|
QString m_pendingChatFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
5
ChatView/icons/new-chat-icon.svg
Normal file
5
ChatView/icons/new-chat-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8Z" fill="black"/>
|
||||||
|
<rect x="11" y="5" width="2" height="9" rx="0.5" fill="black"/>
|
||||||
|
<rect x="7.5" y="8.5" width="9" height="2" rx="0.5" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 526 B |
17
ChatView/icons/open-in-code.svg
Normal file
17
ChatView/icons/open-in-code.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_74_52)">
|
||||||
|
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||||
|
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_74_52)">
|
||||||
|
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
|
||||||
|
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_74_52">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 943 B |
@@ -1,17 +1,6 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_74_52)">
|
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round">
|
||||||
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="black"/>
|
||||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="none"/>
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_74_52)">
|
|
||||||
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
|
|
||||||
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
|
|
||||||
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
|
|
||||||
</g>
|
</g>
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_74_52">
|
|
||||||
<rect width="44" height="44" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 943 B After Width: | Height: | Size: 348 B |
6
ChatView/icons/open-in-window.svg
Normal file
6
ChatView/icons/open-in-window.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round">
|
||||||
|
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="none"/>
|
||||||
|
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 348 B |
5
ChatView/icons/warning-icon.svg
Normal file
5
ChatView/icons/warning-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 3L22 20H2L12 3Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 10V14" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M12 17H12.01" stroke="black" stroke-width="2.4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 350 B |
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -19,6 +20,9 @@ ChatRootView {
|
|||||||
colorGroup: SystemPalette.Active
|
colorGroup: SystemPalette.Active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property bool hasActiveError: false
|
||||||
|
readonly property color errorColor: "#d32f2f"
|
||||||
|
|
||||||
palette {
|
palette {
|
||||||
window: sysPalette.window
|
window: sysPalette.window
|
||||||
windowText: sysPalette.windowText
|
windowText: sysPalette.windowText
|
||||||
@@ -87,11 +91,28 @@ ChatRootView {
|
|||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: childrenRect.height + 10
|
Layout.preferredHeight: childrenRect.height + 10
|
||||||
|
|
||||||
|
isInEditor: root.isInEditor
|
||||||
saveButton.onClicked: root.showSaveDialog()
|
saveButton.onClicked: root.showSaveDialog()
|
||||||
loadButton.onClicked: root.showLoadDialog()
|
loadButton.onClicked: root.showLoadDialog()
|
||||||
clearButton.onClicked: root.clearChat()
|
clearButton.onClicked: root.clearChat()
|
||||||
|
newChatButton.onClicked: root.requestNewChat()
|
||||||
tokensBadge {
|
tokensBadge {
|
||||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
readonly property int sessionPrompt: root.chatModel.sessionPromptTokens || 0
|
||||||
|
readonly property int sessionCompletion: root.chatModel.sessionCompletionTokens || 0
|
||||||
|
readonly property int sessionCached: root.chatModel.sessionCachedPromptTokens || 0
|
||||||
|
text: sessionCached > 0
|
||||||
|
? qsTr("next ~%1 · session ↑%2 ↓%3 ↻%4")
|
||||||
|
.arg(root.inputTokensCount)
|
||||||
|
.arg(sessionPrompt)
|
||||||
|
.arg(sessionCompletion)
|
||||||
|
.arg(sessionCached)
|
||||||
|
: qsTr("next ~%1 · session ↑%2 ↓%3")
|
||||||
|
.arg(root.inputTokensCount)
|
||||||
|
.arg(sessionPrompt)
|
||||||
|
.arg(sessionCompletion)
|
||||||
|
ToolTip.text: sessionCached > 0
|
||||||
|
? qsTr("next request (estimate) · session prompt ↑ / completion ↓ / cached ↻ (provider cache hits)")
|
||||||
|
: qsTr("next request (estimate) · session prompt ↑ / completion ↓")
|
||||||
}
|
}
|
||||||
recentPath {
|
recentPath {
|
||||||
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")
|
||||||
@@ -103,61 +124,91 @@ ChatRootView {
|
|||||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||||
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
||||||
}
|
}
|
||||||
toolsButton {
|
relocateButton {
|
||||||
checked: root.useTools
|
icon.source: (typeof _chatview !== 'undefined')
|
||||||
onCheckedChanged: {
|
? "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
||||||
root.useTools = toolsButton.checked
|
: "qrc:/qt/qml/ChatView/icons/open-in-window.svg"
|
||||||
}
|
onClicked: {
|
||||||
}
|
if (typeof _chatview !== 'undefined')
|
||||||
thinkingMode {
|
root.relocateToSplit()
|
||||||
checked: root.useThinking
|
else
|
||||||
enabled: root.isThinkingSupport
|
root.relocateToWindow()
|
||||||
onCheckedChanged: {
|
|
||||||
root.useThinking = thinkingMode.checked
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
||||||
|
? qsTr("Move this chat to an editor tab")
|
||||||
|
: qsTr("Move this chat to a separate window")
|
||||||
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 {
|
roleSelector {
|
||||||
model: root.availableAgentRoles
|
model: root.availableRoles
|
||||||
displayText: root.currentAgentRole
|
displayText: root.currentRole
|
||||||
onActivated: function(index) {
|
onActivated: function(index) {
|
||||||
root.applyAgentRole(root.availableAgentRoles[index])
|
root.currentRole = root.availableRoles[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: root.loadAvailableRoles()
|
||||||
|
|
||||||
popup.onAboutToShow: {
|
popup.onAboutToShow: {
|
||||||
root.loadAvailableAgentRoles()
|
root.loadAvailableRoles()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
MessageNavigator {
|
||||||
|
id: messageNavigator
|
||||||
|
|
||||||
|
Layout.preferredWidth: 16
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.topMargin: 4
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
|
||||||
|
chatModel: root.chatModel
|
||||||
|
|
||||||
|
onMessageClicked: function(messageIndex) {
|
||||||
|
chatListView.userScrolledUp = true
|
||||||
|
chatListView.positionViewAtIndex(messageIndex, ListView.Beginning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: chatListView
|
id: chatListView
|
||||||
|
|
||||||
property bool userScrolledUp: false
|
property bool userScrolledUp: false
|
||||||
|
|
||||||
|
function syncNavigatorCurrent() {
|
||||||
|
const top = indexAt(10, contentY + 4)
|
||||||
|
messageNavigator.updateCurrentFromModelIndex(top)
|
||||||
|
}
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
leftMargin: 5
|
leftMargin: 3
|
||||||
model: root.chatModel
|
model: root.chatModel
|
||||||
clip: true
|
clip: true
|
||||||
spacing: 0
|
spacing: 0
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
cacheBuffer: 2000
|
cacheBuffer: 2000
|
||||||
|
|
||||||
|
onContentYChanged: Qt.callLater(syncNavigatorCurrent)
|
||||||
|
|
||||||
onMovingChanged: {
|
onMovingChanged: {
|
||||||
if (moving) {
|
if (moving) {
|
||||||
userScrolledUp = !atYEnd
|
userScrolledUp = !atYEnd
|
||||||
@@ -244,6 +295,7 @@ ChatRootView {
|
|||||||
if (!userScrolledUp) {
|
if (!userScrolledUp) {
|
||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
|
Qt.callLater(syncNavigatorCurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentHeightChanged: {
|
onContentHeightChanged: {
|
||||||
@@ -259,6 +311,7 @@ ChatRootView {
|
|||||||
id: chatItemInstance
|
id: chatItemInstance
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
chatViewport: chatListView
|
||||||
msgModel: root.chatModel.processMessageContent(model.content)
|
msgModel: root.chatModel.processMessageContent(model.content)
|
||||||
messageAttachments: model.attachments
|
messageAttachments: model.attachments
|
||||||
messageImages: model.images
|
messageImages: model.images
|
||||||
@@ -270,6 +323,10 @@ ChatRootView {
|
|||||||
codeFontSize: root.codeFontSize
|
codeFontSize: root.codeFontSize
|
||||||
textFontSize: root.textFontSize
|
textFontSize: root.textFontSize
|
||||||
textFormat: root.textFormat
|
textFormat: root.textFormat
|
||||||
|
promptTokens: model.promptTokens || 0
|
||||||
|
completionTokens: model.completionTokens || 0
|
||||||
|
cachedPromptTokens: model.cachedPromptTokens || 0
|
||||||
|
reasoningTokens: model.reasoningTokens || 0
|
||||||
|
|
||||||
onResetChatToMessage: function(idx) {
|
onResetChatToMessage: function(idx) {
|
||||||
messageInput.text = model.content
|
messageInput.text = model.content
|
||||||
@@ -334,6 +391,7 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
id: view
|
id: view
|
||||||
@@ -345,11 +403,10 @@ ChatRootView {
|
|||||||
QQC.TextArea {
|
QQC.TextArea {
|
||||||
id: messageInput
|
id: messageInput
|
||||||
|
|
||||||
placeholderText: Qt.platform.os === "osx"
|
placeholderText: qsTr("Type your message here... (%1 to send)").arg(root.sendShortcutText)
|
||||||
? qsTr("Type your message here... (⌘+↩ to send)")
|
|
||||||
: qsTr("Type your message here... (Ctrl+Enter to send)")
|
|
||||||
placeholderTextColor: palette.mid
|
placeholderTextColor: palette.mid
|
||||||
color: palette.text
|
color: palette.text
|
||||||
|
wrapMode: TextArea.Wrap
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: 2
|
radius: 2
|
||||||
color: palette.base
|
color: palette.base
|
||||||
@@ -372,15 +429,31 @@ ChatRootView {
|
|||||||
root.calculateMessageTokensCount(messageInput.text)
|
root.calculateMessageTokensCount(messageInput.text)
|
||||||
var cursorPos = messageInput.cursorPosition
|
var cursorPos = messageInput.cursorPosition
|
||||||
var textBefore = messageInput.text.substring(0, cursorPos)
|
var textBefore = messageInput.text.substring(0, cursorPos)
|
||||||
|
|
||||||
var atIndex = textBefore.lastIndexOf('@')
|
var atIndex = textBefore.lastIndexOf('@')
|
||||||
if (atIndex >= 0) {
|
if (atIndex >= 0) {
|
||||||
var query = textBefore.substring(atIndex + 1)
|
var query = textBefore.substring(atIndex + 1)
|
||||||
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
||||||
fileMentionPopup.updateSearch(query)
|
fileMentionPopup.updateSearch(query)
|
||||||
|
skillCommandPopup.dismiss()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileMentionPopup.dismiss()
|
fileMentionPopup.dismiss()
|
||||||
|
|
||||||
|
const slashIndex = textBefore.lastIndexOf('/')
|
||||||
|
if (slashIndex >= 0) {
|
||||||
|
const beforeSlash = slashIndex === 0
|
||||||
|
? ' '
|
||||||
|
: textBefore.charAt(slashIndex - 1)
|
||||||
|
const skillQuery = textBefore.substring(slashIndex + 1)
|
||||||
|
if ((beforeSlash === ' ' || beforeSlash === '\n')
|
||||||
|
&& /^[a-z0-9-]*$/.test(skillQuery)) {
|
||||||
|
skillCommandPopup.updateSearch(skillQuery)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skillCommandPopup.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
@@ -398,6 +471,23 @@ ChatRootView {
|
|||||||
fileMentionPopup.dismiss()
|
fileMentionPopup.dismiss()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
}
|
||||||
|
} else if (skillCommandPopup.visible) {
|
||||||
|
if (event.key === Qt.Key_Down) {
|
||||||
|
skillCommandPopup.moveDown()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
skillCommandPopup.moveUp()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
root.applySkillSelection()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
skillCommandPopup.dismiss()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
} else if (root.isSendShortcut(event.key, event.modifiers)) {
|
||||||
|
root.sendChatMessage()
|
||||||
|
event.accepted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,13 +580,21 @@ ChatRootView {
|
|||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 40
|
||||||
|
|
||||||
isCompressing: root.isCompressing
|
isCompressing: root.isCompressing
|
||||||
|
isProcessing: root.isRequestInProgress
|
||||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||||
: root.cancelRequest()
|
: root.cancelRequest()
|
||||||
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
sendButton.icon.source: root.isRequestInProgress
|
||||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
? ""
|
||||||
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
|
: (root.hasActiveError ? "qrc:/qt/qml/ChatView/icons/warning-icon.svg"
|
||||||
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
: "qrc:/qt/qml/ChatView/icons/chat-icon.svg")
|
||||||
: qsTr("Stop")
|
sendButton.text: root.isRequestInProgress ? qsTr("Stop") : qsTr("Send")
|
||||||
|
sendButton.accentColor: (root.hasActiveError && !root.isRequestInProgress)
|
||||||
|
? root.errorColor : "transparent"
|
||||||
|
sendButtonTooltip.text: root.isRequestInProgress
|
||||||
|
? qsTr("Stop")
|
||||||
|
: (root.hasActiveError
|
||||||
|
? root.lastErrorMessage
|
||||||
|
: qsTr("Send message to LLM %1").arg(root.sendShortcutText))
|
||||||
compressButton.onClicked: compressConfirmDialog.open()
|
compressButton.onClicked: compressConfirmDialog.open()
|
||||||
cancelCompressButton.onClicked: root.cancelCompression()
|
cancelCompressButton.onClicked: root.cancelCompression()
|
||||||
syncOpenFiles {
|
syncOpenFiles {
|
||||||
@@ -509,15 +607,6 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Shortcut {
|
|
||||||
id: sendMessageShortcut
|
|
||||||
|
|
||||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
|
||||||
context: Qt.WindowShortcut
|
|
||||||
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
|
|
||||||
onActivated: root.sendChatMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.clearMessages()
|
root.clearMessages()
|
||||||
root.clearAttachmentFiles()
|
root.clearAttachmentFiles()
|
||||||
@@ -528,6 +617,31 @@ ChatRootView {
|
|||||||
Qt.callLater(chatListView.positionViewAtEnd)
|
Qt.callLater(chatListView.positionViewAtEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusInput() {
|
||||||
|
messageInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
property Item focusGuard: Window.activeFocusItem
|
||||||
|
onFocusGuardChanged: Qt.callLater(returnFocusToInputIfNeeded)
|
||||||
|
|
||||||
|
function returnFocusToInputIfNeeded() {
|
||||||
|
var item = Window.activeFocusItem
|
||||||
|
if (!item || item === messageInput)
|
||||||
|
return
|
||||||
|
if (item.cursorVisible !== undefined || item.selectByMouse !== undefined)
|
||||||
|
return
|
||||||
|
if (item.popup !== undefined)
|
||||||
|
return
|
||||||
|
var p = item
|
||||||
|
while (p) {
|
||||||
|
if (p === root) {
|
||||||
|
messageInput.forceActiveFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p = p.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyMentionSelection() {
|
function applyMentionSelection() {
|
||||||
var result = fileMentionPopup.applyCurrentSelection(
|
var result = fileMentionPopup.applyCurrentSelection(
|
||||||
messageInput.text, messageInput.cursorPosition, root.useTools)
|
messageInput.text, messageInput.cursorPosition, root.useTools)
|
||||||
@@ -537,7 +651,25 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySkillSelection() {
|
||||||
|
const name = skillCommandPopup.currentName()
|
||||||
|
if (name === "")
|
||||||
|
return
|
||||||
|
const cursorPos = messageInput.cursorPosition
|
||||||
|
const textBefore = messageInput.text.substring(0, cursorPos)
|
||||||
|
const slashIndex = textBefore.lastIndexOf('/')
|
||||||
|
if (slashIndex < 0)
|
||||||
|
return
|
||||||
|
const before = messageInput.text.substring(0, slashIndex)
|
||||||
|
const after = messageInput.text.substring(cursorPos)
|
||||||
|
const token = '/' + name + ' '
|
||||||
|
messageInput.text = before + token + after
|
||||||
|
messageInput.cursorPosition = before.length + token.length
|
||||||
|
skillCommandPopup.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
|
root.hasActiveError = false
|
||||||
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
fileMentionPopup.clearMentions()
|
fileMentionPopup.clearMentions()
|
||||||
@@ -560,13 +692,122 @@ ChatRootView {
|
|||||||
onAccepted: root.compressCurrentChat()
|
onAccepted: root.compressCurrentChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast {
|
Rectangle {
|
||||||
id: errorToast
|
id: errorBanner
|
||||||
z: 1000
|
|
||||||
|
|
||||||
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
|
z: 1000
|
||||||
border.color: Qt.darker(infoToast.color, 1.3)
|
visible: root.hasActiveError && root.lastErrorMessage.length > 0
|
||||||
toastTextColor: "#FFFFFF"
|
|
||||||
|
width: parent.width / 2
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.bottomMargin: bottomBar.height + 48
|
||||||
|
|
||||||
|
height: visible ? errorRow.implicitHeight + 12 : 0
|
||||||
|
|
||||||
|
color: Qt.rgba(0.83, 0.18, 0.18, 0.96)
|
||||||
|
radius: 6
|
||||||
|
border.color: Qt.darker(color, 1.3)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: errorRow
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 6
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.lastErrorMessage
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.pixelSize: 12
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
selectionColor: Qt.darker(errorBanner.color, 1.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: copyErrorButton
|
||||||
|
|
||||||
|
property bool copied: false
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
implicitWidth: copyErrorLabel.implicitWidth + 18
|
||||||
|
implicitHeight: 22
|
||||||
|
radius: 4
|
||||||
|
color: copyErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28)
|
||||||
|
: Qt.rgba(1, 1, 1, 0.16)
|
||||||
|
border.color: Qt.rgba(1, 1, 1, 0.45)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: copyErrorLabel
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: copyErrorButton.copied ? qsTr("Copied") : qsTr("Copy")
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: copyErrorMouse
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
root.copyToClipboard(root.lastErrorMessage)
|
||||||
|
copyErrorButton.copied = true
|
||||||
|
copyErrorResetTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: copyErrorResetTimer
|
||||||
|
|
||||||
|
interval: 1500
|
||||||
|
onTriggered: copyErrorButton.copied = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: closeErrorButton
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
implicitWidth: 22
|
||||||
|
implicitHeight: 22
|
||||||
|
radius: 4
|
||||||
|
color: closeErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28) : "transparent"
|
||||||
|
border.color: Qt.rgba(1, 1, 1, 0.45)
|
||||||
|
border.width: closeErrorMouse.containsMouse ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "✕"
|
||||||
|
color: "#FFFFFF"
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeErrorMouse
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.hasActiveError = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast {
|
Toast {
|
||||||
@@ -586,27 +827,14 @@ ChatRootView {
|
|||||||
x: (parent.width - width) / 2
|
x: (parent.width - width) / 2
|
||||||
y: (parent.height - height) / 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()
|
onOpenSettings: root.openSettings()
|
||||||
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
|
||||||
onOpenRulesFolder: root.openRulesFolder()
|
|
||||||
onRefreshRules: root.refreshRules()
|
|
||||||
onRuleSelected: function(index) {
|
|
||||||
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onLastErrorMessageChanged() {
|
function onLastErrorMessageChanged() {
|
||||||
if (root.lastErrorMessage.length > 0) {
|
if (root.lastErrorMessage.length > 0) {
|
||||||
errorToast.show(root.lastErrorMessage)
|
root.hasActiveError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onLastInfoMessageChanged() {
|
function onLastInfoMessageChanged() {
|
||||||
@@ -636,7 +864,21 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SkillCommandPopup {
|
||||||
|
id: skillCommandPopup
|
||||||
|
|
||||||
|
z: 999
|
||||||
|
width: Math.min(480, root.width - 20)
|
||||||
|
|
||||||
|
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
|
||||||
|
y: view.y - height - 4
|
||||||
|
|
||||||
|
skillProvider: root
|
||||||
|
|
||||||
|
onSelectionRequested: root.applySkillSelection()
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
messageInput.forceActiveFocus()
|
focusInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ChatView
|
import ChatView
|
||||||
@@ -30,10 +31,16 @@ Rectangle {
|
|||||||
property int textFontSize: Qt.application.font.pointSize
|
property int textFontSize: Qt.application.font.pointSize
|
||||||
property int codeFontSize: Qt.application.font.pointSize
|
property int codeFontSize: Qt.application.font.pointSize
|
||||||
property int textFormat: 0
|
property int textFormat: 0
|
||||||
|
property Flickable chatViewport: null
|
||||||
|
|
||||||
property bool isUserMessage: false
|
property bool isUserMessage: false
|
||||||
property int messageIndex: -1
|
property int messageIndex: -1
|
||||||
|
|
||||||
|
property int promptTokens: 0
|
||||||
|
property int completionTokens: 0
|
||||||
|
property int cachedPromptTokens: 0
|
||||||
|
property int reasoningTokens: 0
|
||||||
|
|
||||||
signal resetChatToMessage(int index)
|
signal resetChatToMessage(int index)
|
||||||
signal openFileRequested(string filePath)
|
signal openFileRequested(string filePath)
|
||||||
|
|
||||||
@@ -135,6 +142,39 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: usageBadge
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 10
|
||||||
|
Layout.rightMargin: 10
|
||||||
|
spacing: 8
|
||||||
|
visible: !root.isUserMessage
|
||||||
|
&& (root.promptTokens > 0 || root.completionTokens > 0)
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.cachedPromptTokens > 0
|
||||||
|
? qsTr("↑ %1 (cached %2)").arg(root.promptTokens).arg(root.cachedPromptTokens)
|
||||||
|
: qsTr("↑ %1").arg(root.promptTokens)
|
||||||
|
color: palette.placeholderText
|
||||||
|
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: root.reasoningTokens > 0
|
||||||
|
? qsTr("↓ %1 (reasoning %2)").arg(root.completionTokens).arg(root.reasoningTokens)
|
||||||
|
: qsTr("↓ %1").arg(root.completionTokens)
|
||||||
|
color: palette.placeholderText
|
||||||
|
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: qsTr("Σ %1").arg(root.promptTokens + root.completionTokens)
|
||||||
|
color: palette.placeholderText
|
||||||
|
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -221,6 +261,7 @@ Rectangle {
|
|||||||
language: itemData.language
|
language: itemData.language
|
||||||
codeFontFamily: root.codeFontFamily
|
codeFontFamily: root.codeFontFamily
|
||||||
codeFontSize: root.codeFontSize
|
codeFontSize: root.codeFontSize
|
||||||
|
viewport: root.chatViewport
|
||||||
}
|
}
|
||||||
|
|
||||||
component AttachmentComponent : Rectangle {
|
component AttachmentComponent : Rectangle {
|
||||||
@@ -315,10 +356,9 @@ Rectangle {
|
|||||||
smooth: true
|
smooth: true
|
||||||
mipmap: true
|
mipmap: true
|
||||||
|
|
||||||
BusyIndicator {
|
QoABusyIndicator {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
running: imageDisplay.status === Image.Loading
|
running: imageDisplay.status === Image.Loading
|
||||||
visible: running
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -13,6 +14,7 @@ Rectangle {
|
|||||||
property string code: ""
|
property string code: ""
|
||||||
property string language: ""
|
property string language: ""
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
|
property Flickable viewport: null
|
||||||
|
|
||||||
property alias codeFontFamily: codeText.font.family
|
property alias codeFontFamily: codeText.font.family
|
||||||
property alias codeFontSize: codeText.font.pointSize
|
property alias codeFontSize: codeText.font.pointSize
|
||||||
@@ -122,7 +124,16 @@ Rectangle {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
|
|
||||||
y: 5
|
y: {
|
||||||
|
if (!root.expanded || !root.viewport)
|
||||||
|
return 5
|
||||||
|
const flick = root.viewport
|
||||||
|
const topInContent = root.mapToItem(flick.contentItem, 0, 0).y
|
||||||
|
const topInView = topInContent - flick.contentY
|
||||||
|
const desired = topInView < 0 ? (-topInView + 5) : 5
|
||||||
|
const maxY = Math.max(5, root.height - copyButton.height - 5)
|
||||||
|
return Math.max(5, Math.min(desired, maxY))
|
||||||
|
}
|
||||||
text: qsTr("Copy")
|
text: qsTr("Copy")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -193,9 +194,24 @@ Rectangle {
|
|||||||
color: root.statusColor
|
color: root.statusColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/open-in-code.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: root.openInEditor(editData.edit_id)
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: parent.hovered
|
||||||
|
delay: 500
|
||||||
|
text: qsTr("Open file in editor and navigate to changes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: headerText
|
id: headerText
|
||||||
Layout.fillWidth: true
|
|
||||||
text: {
|
text: {
|
||||||
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
||||||
if (root.oldContent.length > 0) {
|
if (root.oldContent.length > 0) {
|
||||||
@@ -223,6 +239,19 @@ Rectangle {
|
|||||||
color: palette.mid
|
color: palette.mid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: actionButtons
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: 5
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: !root.isPending
|
visible: !root.isPending
|
||||||
Layout.preferredWidth: badgeText.width + 12
|
Layout.preferredWidth: badgeText.width + 12
|
||||||
@@ -239,31 +268,6 @@ Rectangle {
|
|||||||
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: actionButtons
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 5
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: root.openInEditor(editData.edit_id)
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Open file in editor and navigate to changes")
|
|
||||||
ToolTip.delay: 500
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
icon {
|
icon {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Qt.labs.platform as Platform
|
||||||
import ChatView
|
import ChatView
|
||||||
import UIControls
|
import UIControls
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ Flow {
|
|||||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||||
onClicked: (mouse) => {
|
onClicked: (mouse) => {
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
contextMenu.popup()
|
contextMenu.open()
|
||||||
} else if (mouse.button === Qt.MiddleButton ||
|
} else if (mouse.button === Qt.MiddleButton ||
|
||||||
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
|
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
|
||||||
root.removeFileFromListByIndex(fileItem.index)
|
root.removeFileFromListByIndex(fileItem.index)
|
||||||
@@ -70,23 +72,23 @@ Flow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
Platform.Menu {
|
||||||
id: contextMenu
|
id: contextMenu
|
||||||
|
|
||||||
MenuItem {
|
Platform.MenuItem {
|
||||||
text: "Open in Qt Creator"
|
text: qsTr("Open in Qt Creator")
|
||||||
onTriggered: fileItem.openFileInEditor()
|
onTriggered: fileItem.openFileInEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem {
|
Platform.MenuItem {
|
||||||
text: "Open in External Editor"
|
text: qsTr("Open in External Editor")
|
||||||
onTriggered: fileItem.openFileInExternalEditor()
|
onTriggered: fileItem.openFileInExternalEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuSeparator {}
|
Platform.MenuSeparator {}
|
||||||
|
|
||||||
MenuItem {
|
Platform.MenuItem {
|
||||||
text: "Remove"
|
text: qsTr("Remove")
|
||||||
onTriggered: root.removeFileFromListByIndex(fileItem.index)
|
onTriggered: root.removeFileFromListByIndex(fileItem.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -19,6 +20,8 @@ Rectangle {
|
|||||||
property alias cancelCompressButton: cancelCompressButtonId
|
property alias cancelCompressButton: cancelCompressButtonId
|
||||||
|
|
||||||
property bool isCompressing: false
|
property bool isCompressing: false
|
||||||
|
property bool isProcessing: false
|
||||||
|
property alias sendButtonTooltip: sendButtonTooltipId
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
@@ -45,9 +48,12 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Attach file to message")
|
visible: attachFilesId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Attach file to message")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -58,9 +64,12 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Attach image to message")
|
visible: attachImagesId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Attach image to message")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -71,9 +80,12 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Link file to context")
|
visible: linkFilesId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Link file to context")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
@@ -81,8 +93,10 @@ Rectangle {
|
|||||||
|
|
||||||
text: qsTr("Sync open files")
|
text: qsTr("Sync open files")
|
||||||
|
|
||||||
ToolTip.visible: syncOpenFilesId.hovered
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
visible: syncOpenFilesId.hovered
|
||||||
|
text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -95,7 +109,7 @@ Rectangle {
|
|||||||
visible: root.isCompressing
|
visible: root.isCompressing
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
BusyIndicator {
|
QoABusyIndicator {
|
||||||
id: compressBusyIndicator
|
id: compressBusyIndicator
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -117,9 +131,11 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
QoAToolTip {
|
||||||
ToolTip.delay: 250
|
visible: cancelCompressButtonId.hovered
|
||||||
ToolTip.text: qsTr("Cancel compression")
|
delay: 250
|
||||||
|
text: qsTr("Cancel compression")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,20 +150,41 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
|
visible: compressButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: sendButtonId
|
id: sendButtonId
|
||||||
|
|
||||||
|
leftPadding: root.isProcessing ? 22 : 4
|
||||||
|
|
||||||
icon {
|
icon {
|
||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoABusyIndicator {
|
||||||
|
id: sendBusyIndicator
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 5
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 14
|
||||||
|
height: 14
|
||||||
|
running: root.isProcessing
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
id: sendButtonTooltipId
|
||||||
|
|
||||||
|
visible: sendButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -118,11 +119,13 @@ Rectangle {
|
|||||||
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
|
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
|
||||||
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
|
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
QoAToolTip {
|
||||||
ToolTip.delay: 250
|
visible: applyAllButton.hovered
|
||||||
ToolTip.text: root.hasPendingEdits
|
delay: 250
|
||||||
|
text: root.hasPendingEdits
|
||||||
? qsTr("Apply all pending and rejected edits in this message")
|
? qsTr("Apply all pending and rejected edits in this message")
|
||||||
: qsTr("Reapply all rejected edits in this message")
|
: qsTr("Reapply all rejected edits in this message")
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: root.applyAllClicked()
|
onClicked: root.applyAllClicked()
|
||||||
}
|
}
|
||||||
@@ -134,9 +137,11 @@ Rectangle {
|
|||||||
enabled: root.hasAppliedEdits
|
enabled: root.hasAppliedEdits
|
||||||
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
|
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
QoAToolTip {
|
||||||
ToolTip.delay: 250
|
visible: undoAllButton.hovered
|
||||||
ToolTip.text: qsTr("Undo all applied edits in this message")
|
delay: 250
|
||||||
|
text: qsTr("Undo all applied edits in this message")
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: root.undoAllClicked()
|
onClicked: root.undoAllClicked()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2026 Petr Mironychev
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
188
ChatView/qml/controls/MessageNavigator.qml
Normal file
188
ChatView/qml/controls/MessageNavigator.qml
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import ChatView
|
||||||
|
import UIControls
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: nav
|
||||||
|
|
||||||
|
property var chatModel
|
||||||
|
property var entries: []
|
||||||
|
property color dotColor: "#92BD6C"
|
||||||
|
property int currentMessageIndex: -1
|
||||||
|
|
||||||
|
readonly property int dotCount: entries.length
|
||||||
|
readonly property int verticalPadding: 8
|
||||||
|
readonly property int minDotSpacing: 18
|
||||||
|
readonly property real availableHeight: Math.max(0, height - 2 * verticalPadding)
|
||||||
|
readonly property real naturalHeight: dotCount > 1 ? (dotCount - 1) * minDotSpacing : 0
|
||||||
|
readonly property bool needsScrolling: naturalHeight > availableHeight
|
||||||
|
readonly property real contentHeight: needsScrolling
|
||||||
|
? naturalHeight + 2 * verticalPadding
|
||||||
|
: Math.max(height, 2 * verticalPadding)
|
||||||
|
|
||||||
|
signal messageClicked(int messageIndex)
|
||||||
|
|
||||||
|
implicitWidth: 16
|
||||||
|
|
||||||
|
function rebuild() {
|
||||||
|
entries = chatModel ? chatModel.userMessagePreviews(80) : []
|
||||||
|
Qt.callLater(scrollCurrentIntoView)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentFromModelIndex(modelIdx) {
|
||||||
|
if (modelIdx < 0) {
|
||||||
|
currentMessageIndex = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let best = -1
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
const e = entries[i]
|
||||||
|
if (!e)
|
||||||
|
continue
|
||||||
|
const mi = e.messageIndex
|
||||||
|
if (mi <= modelIdx)
|
||||||
|
best = mi
|
||||||
|
else
|
||||||
|
break
|
||||||
|
}
|
||||||
|
currentMessageIndex = best
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiIndexOf(messageIndex) {
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
const e = entries[i]
|
||||||
|
if (e && e.messageIndex === messageIndex)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function dotCenterY(uiIndex) {
|
||||||
|
const count = dotCount
|
||||||
|
if (count <= 1)
|
||||||
|
return contentHeight / 2
|
||||||
|
const spacing = needsScrolling
|
||||||
|
? minDotSpacing
|
||||||
|
: availableHeight / (count - 1)
|
||||||
|
return verticalPadding + spacing * uiIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollCurrentIntoView() {
|
||||||
|
if (!needsScrolling || currentMessageIndex < 0)
|
||||||
|
return
|
||||||
|
const ui = uiIndexOf(currentMessageIndex)
|
||||||
|
if (ui < 0)
|
||||||
|
return
|
||||||
|
const y = dotCenterY(ui)
|
||||||
|
const margin = 24
|
||||||
|
if (y < flick.contentY + margin)
|
||||||
|
flick.contentY = Math.max(0, y - margin)
|
||||||
|
else if (y > flick.contentY + flick.height - margin)
|
||||||
|
flick.contentY = Math.min(
|
||||||
|
Math.max(0, flick.contentHeight - flick.height),
|
||||||
|
y - flick.height + margin)
|
||||||
|
}
|
||||||
|
|
||||||
|
onChatModelChanged: rebuild()
|
||||||
|
onCurrentMessageIndexChanged: scrollCurrentIntoView()
|
||||||
|
Component.onCompleted: rebuild()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: nav.chatModel
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onRowsInserted() { nav.rebuild() }
|
||||||
|
function onRowsRemoved() { nav.rebuild() }
|
||||||
|
function onModelReset() { nav.rebuild() }
|
||||||
|
function onModelReseted() { nav.rebuild() }
|
||||||
|
function onDataChanged() { nav.rebuild() }
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: flick
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: nav.contentHeight
|
||||||
|
interactive: nav.needsScrolling
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: spine
|
||||||
|
|
||||||
|
visible: nav.dotCount > 1
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
y: nav.verticalPadding
|
||||||
|
width: 1
|
||||||
|
height: Math.max(0, flick.contentHeight - 2 * nav.verticalPadding)
|
||||||
|
color: palette.mid
|
||||||
|
opacity: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: nav.entries
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: dotItem
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property int msgIndex: modelData && modelData.messageIndex !== undefined
|
||||||
|
? modelData.messageIndex : -1
|
||||||
|
readonly property string preview: modelData && modelData.preview !== undefined
|
||||||
|
? modelData.preview : ""
|
||||||
|
readonly property bool isCurrent: nav.currentMessageIndex === msgIndex
|
||||||
|
|
||||||
|
width: 16
|
||||||
|
height: 14
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
y: nav.dotCenterY(index) - height / 2
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: dot
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: dotItem.isCurrent ? 11 : (dotArea.containsMouse ? 10 : 7)
|
||||||
|
height: width
|
||||||
|
radius: width / 2
|
||||||
|
color: dotArea.containsMouse
|
||||||
|
? Qt.lighter(nav.dotColor, 1.2)
|
||||||
|
: nav.dotColor
|
||||||
|
border.color: dotItem.isCurrent
|
||||||
|
? Qt.darker(nav.dotColor, 1.7)
|
||||||
|
: Qt.darker(nav.dotColor, 1.4)
|
||||||
|
border.width: dotItem.isCurrent ? 2 : 1
|
||||||
|
opacity: dotItem.isCurrent || dotArea.containsMouse ? 1.0 : 0.55
|
||||||
|
|
||||||
|
Behavior on width { NumberAnimation { duration: 120 } }
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||||
|
Behavior on color { ColorAnimation { duration: 120 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: dotArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: nav.messageClicked(dotItem.msgIndex)
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: dotArea.containsMouse
|
||||||
|
delay: 350
|
||||||
|
text: dotItem.preview.length > 0
|
||||||
|
? qsTr("#%1 · %2").arg(dotItem.index + 1).arg(dotItem.preview)
|
||||||
|
: qsTr("Jump to message #%1").arg(dotItem.index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
126
ChatView/qml/controls/SkillCommandPopup.qml
Normal file
126
ChatView/qml/controls/SkillCommandPopup.qml
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Object exposing Q_INVOKABLE QVariantList searchSkills(query).
|
||||||
|
property var skillProvider: null
|
||||||
|
property var searchResults: []
|
||||||
|
property int currentIndex: 0
|
||||||
|
|
||||||
|
signal selectionRequested()
|
||||||
|
|
||||||
|
visible: searchResults.length > 0
|
||||||
|
height: Math.min(searchResults.length * 40, 40 * 6) + 2
|
||||||
|
|
||||||
|
color: palette.window
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 4
|
||||||
|
|
||||||
|
function updateSearch(query) {
|
||||||
|
searchResults = skillProvider ? skillProvider.searchSkills(query) : []
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
searchResults = []
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp() {
|
||||||
|
if (currentIndex > 0)
|
||||||
|
currentIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown() {
|
||||||
|
if (currentIndex < searchResults.length - 1)
|
||||||
|
currentIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentName() {
|
||||||
|
if (currentIndex >= 0 && currentIndex < searchResults.length)
|
||||||
|
return searchResults[currentIndex].name
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onCurrentIndexChanged: listView.positionViewAtIndex(currentIndex, ListView.Contain)
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
model: root.searchResults
|
||||||
|
currentIndex: root.currentIndex
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: delegateItem
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: listView.width
|
||||||
|
height: 40
|
||||||
|
color: index === root.currentIndex
|
||||||
|
? palette.highlight
|
||||||
|
: (hoverArea.containsMouse
|
||||||
|
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
|
||||||
|
: "transparent")
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.topMargin: 4
|
||||||
|
anchors.bottomMargin: 4
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "/" + delegateItem.modelData.name
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? palette.highlightedText
|
||||||
|
: palette.text
|
||||||
|
font.bold: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: delegateItem.modelData.description
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? Qt.rgba(palette.highlightedText.r,
|
||||||
|
palette.highlightedText.g,
|
||||||
|
palette.highlightedText.b, 0.7)
|
||||||
|
: palette.mid
|
||||||
|
font.pixelSize: 11
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hoverArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
root.currentIndex = delegateItem.index
|
||||||
|
root.selectionRequested()
|
||||||
|
}
|
||||||
|
onEntered: root.currentIndex = delegateItem.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
@@ -10,19 +11,22 @@ import UIControls
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property bool isInEditor: false
|
||||||
|
|
||||||
property alias saveButton: saveButtonId
|
property alias saveButton: saveButtonId
|
||||||
property alias loadButton: loadButtonId
|
property alias loadButton: loadButtonId
|
||||||
property alias clearButton: clearButtonId
|
property alias clearButton: clearButtonId
|
||||||
|
property alias newChatButton: newChatButtonId
|
||||||
property alias tokensBadge: tokensBadgeId
|
property alias tokensBadge: tokensBadgeId
|
||||||
property alias recentPath: recentPathId
|
property alias recentPath: recentPathId
|
||||||
property alias openChatHistory: openChatHistoryId
|
property alias openChatHistory: openChatHistoryId
|
||||||
property alias pinButton: pinButtonId
|
property alias pinButton: pinButtonId
|
||||||
|
property alias relocateButton: relocateButtonId
|
||||||
property alias contextButton: contextButtonId
|
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 roleSelector: roleSelectorId
|
||||||
|
property alias relocateTooltip: relocateTooltipId
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
@@ -55,93 +59,112 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: checked ? qsTr("Unpin chat window")
|
visible: pinButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: pinButtonId.checked ? qsTr("Unpin chat window")
|
||||||
: qsTr("Pin chat window to the top")
|
: qsTr("Pin chat window to the top")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoAButton {
|
||||||
id: configSelectorId
|
id: relocateButtonId
|
||||||
|
|
||||||
implicitHeight: 25
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
model: []
|
icon {
|
||||||
currentIndex: 0
|
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
QoAToolTip {
|
||||||
ToolTip.delay: 250
|
id: relocateTooltipId
|
||||||
ToolTip.text: qsTr("Switch saved AI configuration")
|
|
||||||
|
visible: relocateButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoASeparator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: clearButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: clearButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Clean chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoASeparator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: newChatButtonId
|
||||||
|
|
||||||
|
visible: root.isInEditor
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/new-chat-icon.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: newChatButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Open new chat in a new tab")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoAComboBox {
|
||||||
id: roleSelector
|
id: agentSelectorId
|
||||||
|
|
||||||
implicitHeight: 25
|
implicitHeight: 25
|
||||||
|
|
||||||
model: []
|
model: []
|
||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
QoAToolTip {
|
||||||
ToolTip.delay: 250
|
visible: agentSelectorId.hovered
|
||||||
ToolTip.text: qsTr("Switch agent role (different system prompts)")
|
delay: 250
|
||||||
|
text: qsTr("Select chat agent (provider and model come from the agent)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAComboBox {
|
||||||
|
id: roleSelectorId
|
||||||
|
|
||||||
|
implicitHeight: 25
|
||||||
|
|
||||||
|
model: []
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: roleSelectorId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Select the role (system prompt) for the chat")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: {
|
|
||||||
if (!toolsButtonId.enabled) {
|
|
||||||
return qsTr("Tools are disabled in General Settings")
|
|
||||||
}
|
|
||||||
return 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
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: enabled ? (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
|
||||||
|
|
||||||
@@ -154,9 +177,11 @@ Rectangle {
|
|||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
QoAToolTip {
|
||||||
ToolTip.delay: 250
|
visible: settingsButtonId.hovered
|
||||||
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
delay: 250
|
||||||
|
text: qsTr("Open Chat Assistant Settings")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoASeparator {
|
QoASeparator {
|
||||||
@@ -182,9 +207,11 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
ToolTip.visible: containsMouse
|
QoAToolTip {
|
||||||
ToolTip.delay: 500
|
visible: parent.containsMouse && recentPathId.text.length > 0
|
||||||
ToolTip.text: recentPathId.text
|
text: recentPathId.text
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,9 +232,12 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Save chat to *.json file")
|
visible: saveButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Save chat to *.json file")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -218,9 +248,12 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Load chat from *.json file")
|
visible: loadButtonId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Load chat from *.json file")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -231,9 +264,12 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Show in system")
|
visible: openChatHistoryId.hovered
|
||||||
|
delay: 250
|
||||||
|
text: qsTr("Show in system")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QoASeparator {}
|
QoASeparator {}
|
||||||
@@ -248,9 +284,11 @@ Rectangle {
|
|||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
QoAToolTip {
|
||||||
ToolTip.delay: 250
|
visible: contextButtonId.hovered
|
||||||
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
|
delay: 250
|
||||||
|
text: qsTr("View chat context (system prompt, role, rules)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Badge {
|
Badge {
|
||||||
@@ -260,21 +298,6 @@ Rectangle {
|
|||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoASeparator {}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: clearButtonId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
|
||||||
height: 15
|
|
||||||
width: 8
|
|
||||||
}
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: qsTr("Clean chat")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,235 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
|
||||||
|
|
||||||
#include <settings/ButtonAspect.hpp>
|
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
#include "QodeAssisttr.h"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
ConfigurationManager &ConfigurationManager::instance()
|
|
||||||
{
|
|
||||||
static ConfigurationManager instance;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::init()
|
|
||||||
{
|
|
||||||
setupConnections();
|
|
||||||
updateAllTemplateDescriptions();
|
|
||||||
checkAllTemplate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (!templ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplateDescription.setValue(templ->description());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
|
|
||||||
m_generalSettings.qrTemplateDescription.setValue(templ->description());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::updateAllTemplateDescriptions()
|
|
||||||
{
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
|
||||||
{
|
|
||||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
|
||||||
|
|
||||||
if (templ->name() == templateAspect.value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (&templateAspect == &m_generalSettings.ccTemplate) {
|
|
||||||
m_generalSettings.ccTemplate.setValue(templ->name());
|
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
|
||||||
m_generalSettings.caTemplate.setValue(templ->name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::checkAllTemplate()
|
|
||||||
{
|
|
||||||
checkTemplate(m_generalSettings.ccTemplate);
|
|
||||||
checkTemplate(m_generalSettings.caTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_generalSettings(Settings::generalSettings())
|
|
||||||
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
|
|
||||||
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
|
|
||||||
{}
|
|
||||||
|
|
||||||
void ConfigurationManager::setupConnections()
|
|
||||||
{
|
|
||||||
using Config = ConfigurationManager;
|
|
||||||
using Button = ButtonAspect;
|
|
||||||
|
|
||||||
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
|
|
||||||
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
|
|
||||||
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
|
|
||||||
connect(
|
|
||||||
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
|
|
||||||
|
|
||||||
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
|
|
||||||
updateTemplateDescription(m_generalSettings.qrTemplate);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectProvider()
|
|
||||||
{
|
|
||||||
const auto providersList = m_providersManager.providersNames();
|
|
||||||
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
|
|
||||||
? m_generalSettings.ccProvider
|
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
|
|
||||||
? m_generalSettings.ccPreset1Provider
|
|
||||||
: settingsButton == &m_generalSettings.qrSelectProvider
|
|
||||||
? m_generalSettings.qrProvider
|
|
||||||
: m_generalSettings.caProvider;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectModel()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
|
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
|
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
|
|
||||||
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
|
||||||
|
|
||||||
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
|
||||||
: m_generalSettings.caUrl.volatileValue();
|
|
||||||
|
|
||||||
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrModel
|
|
||||||
: m_generalSettings.caModel);
|
|
||||||
|
|
||||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
|
||||||
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
|
|
||||||
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
provider->getInstalledModels(providerUrl)
|
|
||||||
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
|
||||||
if (modelList.isEmpty()) {
|
|
||||||
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectTemplate()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
|
|
||||||
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
|
|
||||||
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
|
|
||||||
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
|
|
||||||
: m_generalSettings.caProvider.volatileValue();
|
|
||||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
|
||||||
|
|
||||||
const auto templateList = isCodeCompletion || isPreset1
|
|
||||||
? m_templateManger.getFimTemplatesForProvider(providerID)
|
|
||||||
: m_templateManger.getChatTemplatesForProvider(providerID);
|
|
||||||
|
|
||||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
|
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Template
|
|
||||||
: isQuickRefactor ? m_generalSettings.qrTemplate
|
|
||||||
: m_generalSettings.caTemplate;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
|
|
||||||
m_generalSettings.showSelectionDialog(
|
|
||||||
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConfigurationManager::selectUrl()
|
|
||||||
{
|
|
||||||
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
|
|
||||||
if (!settingsButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QStringList urls;
|
|
||||||
for (const auto &name : m_providersManager.providersNames()) {
|
|
||||||
const auto url = m_providersManager.getProviderByName(name)->url();
|
|
||||||
if (!urls.contains(url))
|
|
||||||
urls.append(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
|
|
||||||
: settingsButton == &m_generalSettings.ccPreset1SetUrl
|
|
||||||
? m_generalSettings.ccPreset1Url
|
|
||||||
: settingsButton == &m_generalSettings.qrSetUrl
|
|
||||||
? m_generalSettings.qrUrl
|
|
||||||
: m_generalSettings.caUrl;
|
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
|
|
||||||
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
#include "pluginllmcore/PromptTemplateManager.hpp"
|
|
||||||
#include "pluginllmcore/ProvidersManager.hpp"
|
|
||||||
#include "settings/GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
|
||||||
|
|
||||||
class ConfigurationManager : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
static ConfigurationManager &instance();
|
|
||||||
|
|
||||||
void init();
|
|
||||||
|
|
||||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
|
||||||
void updateAllTemplateDescriptions();
|
|
||||||
void checkTemplate(const Utils::StringAspect &templateAspect);
|
|
||||||
void checkAllTemplate();
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void selectProvider();
|
|
||||||
void selectModel();
|
|
||||||
void selectTemplate();
|
|
||||||
void selectUrl();
|
|
||||||
|
|
||||||
private:
|
|
||||||
explicit ConfigurationManager(QObject *parent = nullptr);
|
|
||||||
~ConfigurationManager() = default;
|
|
||||||
ConfigurationManager(const ConfigurationManager &) = delete;
|
|
||||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
|
||||||
|
|
||||||
Settings::GeneralSettings &m_generalSettings;
|
|
||||||
PluginLLMCore::ProvidersManager &m_providersManager;
|
|
||||||
PluginLLMCore::PromptTemplateManager &m_templateManger;
|
|
||||||
|
|
||||||
void setupConnections();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist
|
|
||||||
21
LICENSE
21
LICENSE
@@ -1,3 +1,24 @@
|
|||||||
|
===============================================================
|
||||||
|
ADDITIONAL TERMS UNDER GPLv3 SECTION 7(b)
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
In accordance with Section 7(b) of the GNU General Public License v3.0,
|
||||||
|
the following additional attribution term applies to QodeAssist:
|
||||||
|
|
||||||
|
You must preserve all author attributions, copyright notices, and the
|
||||||
|
project name "QodeAssist" in all copies and modified versions,
|
||||||
|
including in source file headers, the plugin metadata
|
||||||
|
(QodeAssist.json.in), and the About dialog or equivalent user-facing
|
||||||
|
identification. Modified versions must be clearly marked as different
|
||||||
|
from the original.
|
||||||
|
|
||||||
|
This is a reasonable attribution requirement permitted under GPLv3
|
||||||
|
§7(b) and §7(c). It supplements the notice-preservation obligations of
|
||||||
|
§4 and §5.
|
||||||
|
|
||||||
|
Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
===============================================================
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
|
|
||||||
@@ -8,27 +9,47 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
|
#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 "sources/common/ContextData.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include "context/DocumentContextReader.hpp"
|
#include "context/DocumentContextReader.hpp"
|
||||||
#include "context/Utils.hpp"
|
#include "context/Utils.hpp"
|
||||||
#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))
|
||||||
@@ -50,33 +71,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::handleRequestFailed(const QString &requestId, const QString &error)
|
void LLMClientInterface::onCompletionFailed(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
|
||||||
@@ -84,9 +106,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)
|
||||||
@@ -119,26 +153,15 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
|
|
||||||
void LLMClientInterface::handleCancelRequest()
|
void LLMClientInterface::handleCancelRequest()
|
||||||
{
|
{
|
||||||
QSet<PluginLLMCore::Provider *> providers;
|
const auto requests = m_activeRequests;
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.value().provider) {
|
|
||||||
providers.insert(it.value().provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
|
||||||
disconnect(provider->client(), nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
const RequestContext &ctx = it.value();
|
|
||||||
if (ctx.provider) {
|
|
||||||
ctx.provider->cancelRequest(it.key());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,124 +244,87 @@ 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::ContextData context = prepareContext(request, documentInfo);
|
||||||
|
|
||||||
if (!promptTemplate) {
|
QString editorContext;
|
||||||
QString error = QString("No template found with name: %1").arg(templateName);
|
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), /*toolsOverride=*/false);
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
QString error = QString("Failed to start completion request for agent '%1': %2")
|
||||||
|
.arg(agentName, session->lastError().message);
|
||||||
|
session->deleteLater();
|
||||||
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::requestFailed,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
auto requestId
|
|
||||||
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
m_performanceLogger.startTimeMeasurement(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const
|
||||||
|
{
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::load().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();
|
||||||
@@ -352,14 +338,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;
|
||||||
@@ -368,15 +346,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;
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#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>
|
||||||
@@ -21,6 +22,14 @@ 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
|
||||||
@@ -29,8 +38,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;
|
||||||
@@ -50,10 +59,6 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
void startImpl() override;
|
void startImpl() override;
|
||||||
|
|
||||||
private slots:
|
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
|
||||||
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);
|
||||||
@@ -63,22 +68,26 @@ private:
|
|||||||
void handleCancelRequest();
|
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::CodeCompletionSettings &m_completeSettings;
|
||||||
const Settings::GeneralSettings &m_generalSettings;
|
const Settings::GeneralSettings &m_generalSettings;
|
||||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
AgentFactory &m_agentFactory;
|
||||||
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
SessionManager &m_sessionManager;
|
||||||
Context::IDocumentReader &m_documentReader;
|
Context::IDocumentReader &m_documentReader;
|
||||||
IRequestPerformanceLogger &m_performanceLogger;
|
IRequestPerformanceLogger &m_performanceLogger;
|
||||||
QElapsedTimer m_completionTimer;
|
QElapsedTimer m_completionTimer;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.9.12",
|
"Version" : "0.9.20",
|
||||||
"CompatVersion" : "${IDE_VERSION}",
|
"CompatVersion" : "${IDE_VERSION}",
|
||||||
"Vendor" : "Petr Mironychev",
|
"Vendor" : "Petr Mironychev",
|
||||||
"VendorId" : "petrmironychev",
|
"VendorId" : "petrmironychev",
|
||||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||||
"License" : "GPLv3",
|
"License" : "GPLv3 with additional attribution terms (§7b) — see LICENSE",
|
||||||
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
||||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||||
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "QodeAssistClient.hpp"
|
#include "QodeAssistClient.hpp"
|
||||||
@@ -157,6 +159,16 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
|
||||||
|
{
|
||||||
|
m_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
|
||||||
|
{
|
||||||
|
m_agentFactory = agentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
@@ -261,9 +273,8 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
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;
|
||||||
@@ -307,9 +318,8 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (m_llmClient->contextManager()
|
if (m_llmClient->contextManager()->shouldIgnore(
|
||||||
->ignoreManager()
|
editor->textDocument()->filePath().toUrlishString())) {
|
||||||
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
|
||||||
.arg(editor->textDocument()->filePath().toUrlishString()));
|
.arg(editor->textDocument()->filePath().toUrlishString()));
|
||||||
return;
|
return;
|
||||||
@@ -317,6 +327,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,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
#include "LSPCompletion.hpp"
|
#include "LSPCompletion.hpp"
|
||||||
@@ -15,11 +17,12 @@
|
|||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
#include "widgets/RefactorWidgetHandler.hpp"
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
#include <pluginllmcore/IPromptProvider.hpp>
|
|
||||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
class SessionManager;
|
||||||
|
class AgentFactory;
|
||||||
|
|
||||||
class QodeAssistClient : public LanguageClient::Client
|
class QodeAssistClient : public LanguageClient::Client
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -27,6 +30,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;
|
||||||
|
|
||||||
@@ -67,6 +73,8 @@ private:
|
|||||||
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
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -10,4 +11,16 @@ const char MENU_ID[] = "QodeAssist.Menu";
|
|||||||
|
|
||||||
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
|
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
|
||||||
|
|
||||||
|
const char QODE_ASSIST_CHAT_CONTEXT[] = "QodeAssist.ChatContext";
|
||||||
|
const char QODE_ASSIST_CHAT_NAV_ID[] = "QodeAssistChat";
|
||||||
|
const char QODE_ASSIST_CHAT_EDITOR_ID[] = "QodeAssist.ChatEditor";
|
||||||
|
|
||||||
|
const char QODE_ASSIST_SHOW_CHAT_ACTION[] = "QodeAssist.ShowChatView";
|
||||||
|
const char QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION[] = "QodeAssist.OpenChatWindow";
|
||||||
|
const char QODE_ASSIST_NEW_CHAT_ACTION[] = "QodeAssist.NewChat";
|
||||||
|
|
||||||
|
const char QODE_ASSIST_CHAT_SEND_MESSAGE[] = "QodeAssist.Chat.SendMessage";
|
||||||
|
const char QODE_ASSIST_CHAT_CLEAR_SESSION[] = "QodeAssist.Chat.ClearSession";
|
||||||
|
const char QODE_ASSIST_CHAT_SHOW_IN_RIGHT[] = "QodeAssist.Chat.ShowInRightSidebar";
|
||||||
|
|
||||||
} // namespace QodeAssist::Constants
|
} // namespace QodeAssist::Constants
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,43 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "QuickRefactorHandler.hpp"
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <LLMQore/ContentBlocks.hpp>
|
||||||
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
#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/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 <sources/common/ResponseCleaner.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/ContextData.hpp"
|
||||||
|
|
||||||
|
#include <AgentFactory.hpp>
|
||||||
|
#include <AgentRouter.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 {
|
||||||
|
|
||||||
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
|
||||||
@@ -33,6 +50,16 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -87,95 +114,109 @@ void QuickRefactorHandler::sendRefactorRequest(
|
|||||||
prepareAndSendRequest(editor, instructions, range);
|
prepareAndSendRequest(editor, instructions, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString QuickRefactorHandler::pickRefactorAgent(const QString &filePath) const
|
||||||
|
{
|
||||||
|
const QStringList roster = Settings::PipelinesConfig::load().rosters.quickRefactor;
|
||||||
|
if (roster.isEmpty() || !m_agentFactory)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
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 filePath = editor->textDocument()->filePath().toUrlishString();
|
||||||
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
|
const QString agentName = pickRefactorAgent(filePath);
|
||||||
|
if (agentName.isEmpty()) {
|
||||||
if (!promptTemplate) {
|
emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath));
|
||||||
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();
|
const bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
if (enableTools) {
|
||||||
provider->prepareRequest(
|
m_sessionManager->toolContributors().contribute(client->tools());
|
||||||
payload,
|
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
|
||||||
promptTemplate,
|
}
|
||||||
context,
|
|
||||||
PluginLLMCore::RequestType::QuickRefactoring,
|
|
||||||
enableTools,
|
|
||||||
enableThinking);
|
|
||||||
|
|
||||||
provider->client()->setMaxToolContinuations(
|
session->systemPrompt()->setLayer(
|
||||||
Settings::toolsSettings().maxToolContinuations());
|
QStringLiteral("refactor"), buildSystemPrompt(editor, range));
|
||||||
|
|
||||||
|
client->setTransferTimeout(
|
||||||
|
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
||||||
|
|
||||||
m_isRefactoringInProgress = true;
|
m_isRefactoringInProgress = true;
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
session, &Session::finished, this,
|
||||||
&::LLMQore::BaseClient::requestCompleted,
|
[this](const LLMQore::RequestID &id, const QString &) { onRefactorFinished(id); });
|
||||||
this,
|
|
||||||
&QuickRefactorHandler::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
session, &Session::failed, this,
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
|
||||||
this,
|
onRefactorFailed(id, error);
|
||||||
&QuickRefactorHandler::handleRequestFailed,
|
});
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
const QString userMessage = instructions.isEmpty()
|
||||||
: promptTemplate->endpoint();
|
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
|
||||||
auto requestId
|
: instructions;
|
||||||
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
|
||||||
m_lastRequestId = requestId;
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
const LLMQore::RequestID requestId = session->send(std::move(blocks), enableTools);
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
m_isRefactoringInProgress = false;
|
||||||
|
const QString reason = session->lastError().message;
|
||||||
|
m_sessionManager->removeSession(session);
|
||||||
|
emitError(QStringLiteral("Failed to start quick refactor request for agent '%1': %2")
|
||||||
|
.arg(agentName, reason));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
m_lastRequestId = requestId;
|
||||||
TextEditor::TextEditorWidget *editor,
|
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
|
||||||
const Utils::Text::Range &range,
|
}
|
||||||
const QString &instructions)
|
|
||||||
|
QString QuickRefactorHandler::buildSystemPrompt(
|
||||||
|
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
|
||||||
{
|
{
|
||||||
PluginLLMCore::ContextData context;
|
Q_UNUSED(range)
|
||||||
|
|
||||||
auto textDocument = editor->textDocument();
|
auto textDocument = editor->textDocument();
|
||||||
Context::DocumentReaderQtCreator documentReader;
|
Context::DocumentReaderQtCreator documentReader;
|
||||||
@@ -183,7 +224,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 Settings::quickRefactorSettings().systemPrompt();
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextCursor cursor = editor->textCursor();
|
QTextCursor cursor = editor->textCursor();
|
||||||
@@ -259,17 +300,6 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
|
|
||||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
||||||
|
|
||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
||||||
if (project) {
|
|
||||||
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
|
||||||
project, PluginLLMCore::RulesContext::QuickRefactor);
|
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
|
||||||
LOG_MESSAGE("Loaded project rules for quick refactor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
systemPrompt += "\n\nFile information:";
|
systemPrompt += "\n\nFile information:";
|
||||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||||
@@ -338,28 +368,52 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||||
}
|
}
|
||||||
|
|
||||||
context.systemPrompt = systemPrompt;
|
return systemPrompt;
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
|
||||||
messages.append(
|
|
||||||
{"user",
|
|
||||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
|
||||||
: instructions});
|
|
||||||
context.history = messages;
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::handleLLMResponse(
|
void QuickRefactorHandler::cancelRequest()
|
||||||
const QString &response, const QJsonObject &request, bool isComplete)
|
|
||||||
{
|
{
|
||||||
if (request["id"].toString() != m_lastRequestId) {
|
if (!m_isRefactoringInProgress)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const auto id = m_lastRequestId;
|
||||||
|
m_isRefactoringInProgress = false;
|
||||||
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
|
auto it = m_activeRequests.find(id);
|
||||||
|
if (it != m_activeRequests.end()) {
|
||||||
|
Session *session = it.value().session;
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefactorResult result;
|
||||||
|
result.success = false;
|
||||||
|
result.errorMessage = "Refactoring request was cancelled";
|
||||||
|
emit refactoringCompleted(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
|
||||||
|
{
|
||||||
|
if (requestId != m_lastRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||||
|
if (it != m_activeRequests.end())
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
|
||||||
|
QString fullText;
|
||||||
|
if (session) {
|
||||||
|
if (auto *history = session->history(); history && !history->isEmpty())
|
||||||
|
fullText = history->messages().back().text();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isComplete) {
|
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
|
const QString cleanedResponse = ResponseCleaner::clean(fullText);
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.newText = cleanedResponse;
|
result.newText = cleanedResponse;
|
||||||
@@ -373,52 +427,33 @@ void QuickRefactorHandler::handleLLMResponse(
|
|||||||
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
|
||||||
|
|
||||||
emit refactoringCompleted(result);
|
emit refactoringCompleted(result);
|
||||||
}
|
|
||||||
|
if (session && m_sessionManager)
|
||||||
|
m_sessionManager->release(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::cancelRequest()
|
void QuickRefactorHandler::onRefactorFailed(
|
||||||
|
const QString &requestId, const QodeAssist::ErrorInfo &error)
|
||||||
{
|
{
|
||||||
if (m_isRefactoringInProgress) {
|
if (requestId != m_lastRequestId)
|
||||||
auto id = m_lastRequestId;
|
return;
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
auto it = m_activeRequests.find(requestId);
|
||||||
if (it.key() == id) {
|
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
|
||||||
const RequestContext &ctx = it.value();
|
if (it != m_activeRequests.end())
|
||||||
ctx.provider->cancelRequest(id);
|
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
|
m_lastRequestId.clear();
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.success = false;
|
result.success = false;
|
||||||
result.errorMessage = "Refactoring request was cancelled";
|
result.errorMessage = error.message;
|
||||||
emit refactoringCompleted(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
|
||||||
{
|
|
||||||
if (requestId == m_lastRequestId) {
|
|
||||||
m_activeRequests.remove(requestId);
|
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
handleLLMResponse(fullText, request, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
|
||||||
{
|
|
||||||
if (requestId == m_lastRequestId) {
|
|
||||||
m_activeRequests.remove(requestId);
|
|
||||||
m_isRefactoringInProgress = false;
|
|
||||||
RefactorResult result;
|
|
||||||
result.success = false;
|
|
||||||
result.errorMessage = error;
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
|
#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;
|
||||||
@@ -33,6 +39,9 @@ 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();
|
||||||
@@ -41,28 +50,26 @@ public:
|
|||||||
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 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 buildSystemPrompt(
|
||||||
const Utils::Text::Range &range,
|
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
|
||||||
const QString &instructions);
|
QString pickRefactorAgent(const QString &filePath) const;
|
||||||
|
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
PluginLLMCore::Provider *provider;
|
QPointer<Session> session;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QPointer<SessionManager> m_sessionManager;
|
||||||
|
QPointer<AgentFactory> m_agentFactory;
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
TextEditor::TextEditorWidget *m_currentEditor;
|
TextEditor::TextEditorWidget *m_currentEditor;
|
||||||
Utils::Text::Range m_currentRange;
|
Utils::Text::Range m_currentRange;
|
||||||
|
|||||||
127
README.md
127
README.md
@@ -6,7 +6,7 @@
|
|||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://discord.gg/BGMkUsXUgf)
|
[](https://discord.gg/BGMkUsXUgf)
|
||||||
|
|
||||||
 **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
|
 **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral, Qwen, DeepSeek), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
|
||||||
|
|
||||||
⚠️ **Important Notice About Paid Providers**
|
⚠️ **Important Notice About Paid Providers**
|
||||||
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
||||||
@@ -35,10 +35,12 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
|
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
|
||||||
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
|
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
|
||||||
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
|
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
|
||||||
|
- **Agent Skills** — reusable folders of specialized instructions loaded on demand; discovered from `.qodeassist/skills/` and `.claude/skills/`, invoked automatically, with `/skill`, or always-on
|
||||||
- **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, any OpenAI-compatible endpoint
|
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint
|
||||||
|
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
|
||||||
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
|
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, 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!
|
||||||
@@ -53,6 +55,11 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
|
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Chat View Mode: (click to expand)</summary>
|
||||||
|
<img src="https://github.com/user-attachments/assets/5914dd78-c8a4-4d35-889a-10ec493d4c4b" width="600" alt="QodeAssistChat2">
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Multiline Code completion: (click to expand)</summary>
|
<summary>Multiline Code completion: (click to expand)</summary>
|
||||||
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
|
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
|
||||||
@@ -85,7 +92,27 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
|
|
||||||
## Install plugin to QtCreator
|
## Install plugin to QtCreator
|
||||||
|
|
||||||
### Method 1: Using QodeAssistUpdater (Beta)
|
### Method 1: Using the Extension Registry (Recommended)
|
||||||
|
|
||||||
|
You can install and update QodeAssist directly from within Qt Creator by adding the QodeAssist registry as an external extension repository.
|
||||||
|
|
||||||
|
1. Open the Extensions page (`Qt Creator → Extensions`) and switch to the **Browser** tab
|
||||||
|
2. Enable **Use External Repository**
|
||||||
|
3. Next to **Repository URLs**, click **Add** and paste the registry archive URL matching your Qt Creator version:
|
||||||
|
- **Latest (QtC 19)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
|
||||||
|
- **QtC 19**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc19.tar.gz`
|
||||||
|
- **QtC 18**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc18.tar.gz`
|
||||||
|
<details>
|
||||||
|
<summary>Example of extension registry: (click to expand)</summary>
|
||||||
|
<img width="600" alt="RegistryExample" src="https://github.com/user-attachments/assets/8ab8cf10-72e7-4961-8c5a-21d530378a05">
|
||||||
|
</details>
|
||||||
|
|
||||||
|
4. Click **Apply** — QodeAssist will appear in the extensions list, where you can **Install** it
|
||||||
|
5. Updates can be installed from the same screen when a new version is published
|
||||||
|
|
||||||
|
> **Note:** This is an external repository not maintained by The Qt Company. By adding it you accept responsibility for managing the associated risks, as stated in the Extensions page.
|
||||||
|
|
||||||
|
### Method 2: Using QodeAssistUpdater (Beta)
|
||||||
|
|
||||||
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
|
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
|
||||||
|
|
||||||
@@ -113,7 +140,7 @@ Download pre-built binary from [QodeAssistUpdater releases](https://github.com/P
|
|||||||
|
|
||||||
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
|
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
|
||||||
|
|
||||||
### Method 2: Manual Installation
|
### Method 3: Manual Installation
|
||||||
|
|
||||||
1. Install Latest Qt Creator
|
1. Install Latest Qt Creator
|
||||||
2. Download the QodeAssist plugin for your Qt Creator
|
2. Download the QodeAssist plugin for your Qt Creator
|
||||||
@@ -143,6 +170,8 @@ The Quick Setup feature provides one-click configuration for popular cloud AI mo
|
|||||||
- **OpenAI** (gpt-5.2-codex)
|
- **OpenAI** (gpt-5.2-codex)
|
||||||
- **Mistral AI** (Codestral 2501)
|
- **Mistral AI** (Codestral 2501)
|
||||||
- **Google AI** (Gemini 2.5 Flash)
|
- **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
|
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!
|
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
||||||
@@ -163,6 +192,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
|
||||||
|
- **DeepSeek** — `deepseek-chat` and `deepseek-reasoner` (reasoning shown as thinking)
|
||||||
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
||||||
|
|
||||||
### Recommended Models for Best Experience
|
### Recommended Models for Best Experience
|
||||||
@@ -185,9 +216,9 @@ For optimal coding assistance, we recommend using these top-tier models:
|
|||||||
|
|
||||||
### Additional Configuration
|
### Additional Configuration
|
||||||
|
|
||||||
|
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents or override bundled ones with TOML profiles
|
||||||
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
||||||
- **[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
|
||||||
@@ -227,7 +258,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
|||||||
- **[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)
|
||||||
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
- Extended thinking / reasoning mode - shows streamed chain-of-thought for reasoning models (Claude, Google, OpenAI Responses, and OpenAI-compatible endpoints returning `reasoning_content` such as DeepSeek, Qwen, LM Studio, OpenRouter)
|
||||||
|
|
||||||
### Quick Refactoring
|
### Quick Refactoring
|
||||||
- Inline code refactoring directly in the editor with AI assistance
|
- Inline code refactoring directly in the editor with AI assistance
|
||||||
@@ -253,6 +284,41 @@ Chat and Quick Refactor can call tools to inspect and modify your project. Each
|
|||||||
| `execute_terminal_command` | Run a shell command (with confirmation) |
|
| `execute_terminal_command` | Run a shell command (with confirmation) |
|
||||||
| `todo_tool` | Track multi-step task progress during a conversation |
|
| `todo_tool` | Track multi-step task progress during a conversation |
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
|
||||||
|
**Agent Skills** package specialized instructions and workflows into reusable folders the AI loads on demand. QodeAssist implements the open [Agent Skills](https://agentskills.io) format, so skills authored for Claude Code, Cursor, or other agents work as-is.
|
||||||
|
|
||||||
|
A skill is a folder containing a `SKILL.md` file — YAML frontmatter (`name`, `description`) plus Markdown instructions:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-skill/
|
||||||
|
└── SKILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-skill
|
||||||
|
description: What the skill does and when to use it.
|
||||||
|
---
|
||||||
|
|
||||||
|
# My Skill
|
||||||
|
|
||||||
|
Step-by-step instructions for the task...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Where skills are discovered:**
|
||||||
|
- **Project skills** — project-relative subdirectories (default `.qodeassist/skills/` and `.claude/skills/`), configured in `Projects → QodeAssist → Skills`. Project skills win over global ones on a name collision.
|
||||||
|
- **Global skills** — absolute directories shared across all projects (default includes `~/.claude/skills/`), configured in `Tools → Options → QodeAssist → Skills`.
|
||||||
|
|
||||||
|
Both settings pages show the list of currently discovered skills.
|
||||||
|
|
||||||
|
**How skills are used in Chat:**
|
||||||
|
- **Automatically** — each skill's name and description is added to the system prompt; when a request matches, the model loads the full instructions via the `load_skill` tool (requires a tool-calling model).
|
||||||
|
- **Explicitly** — type `/` in the chat input and pick a skill from the popup; its instructions are injected into that one message. Works with any model.
|
||||||
|
- **Always-on** — a skill whose frontmatter has `metadata: always-on: "true"` is injected into every chat request automatically.
|
||||||
|
|
||||||
|
Enable or disable the whole feature in `Tools → Options → QodeAssist → Skills`.
|
||||||
|
|
||||||
### MCP Server
|
### MCP Server
|
||||||
|
|
||||||
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
|
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
|
||||||
@@ -407,7 +473,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
- **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 enabled
|
||||||
|
|
||||||
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 [Agent Roles Guide](docs/agent-roles.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
@@ -454,6 +520,7 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
|
|||||||
- [x] Quick refactoring with custom-instructions library
|
- [x] Quick refactoring with custom-instructions library
|
||||||
- [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] Project-specific rules (`.qodeassist/rules/`)
|
- [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)
|
||||||
@@ -465,11 +532,12 @@ 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.
|
||||||
|
|
||||||
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
||||||
|
- Paypal: [my paypalme page](https://www.paypal.com/paypalme/palm1r)
|
||||||
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
||||||
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
||||||
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
||||||
@@ -512,6 +580,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
|
||||||
@@ -522,6 +594,45 @@ cmake --build .
|
|||||||
|
|
||||||
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
|
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
QodeAssist is licensed under the **GNU General Public License v3.0**
|
||||||
|
(see [`LICENSE`](LICENSE)), with **additional attribution terms under
|
||||||
|
GPLv3 Section 7(b)**.
|
||||||
|
|
||||||
|
You are free to use, modify, and redistribute QodeAssist under GPL-3.0,
|
||||||
|
but you **must preserve** the original author attribution, copyright
|
||||||
|
notices, and project identification — including in source file headers,
|
||||||
|
the plugin metadata (`QodeAssist.json.in`), and the About dialog or
|
||||||
|
equivalent user-facing identification. Modified versions must be clearly
|
||||||
|
marked as different from the original.
|
||||||
|
|
||||||
|
### Commercial licensing
|
||||||
|
|
||||||
|
QodeAssist is also available under a separate commercial license for use
|
||||||
|
in proprietary or closed-source products without GPL-3.0 obligations.
|
||||||
|
For commercial licensing inquiries, contact **palm1r-github-dev@pm.me**.
|
||||||
|
|
||||||
|
### Qt Creator components and attributions
|
||||||
|
|
||||||
|
QodeAssist is a plugin for Qt Creator and incorporates certain components
|
||||||
|
(plugin templates, API headers, and related boilerplate) originating from
|
||||||
|
Qt Creator, which are copyright (C) The Qt Company Ltd.
|
||||||
|
|
||||||
|
These components are provided by The Qt Company under the GNU General
|
||||||
|
Public License version 3, annotated with **The Qt Company GPL Exception
|
||||||
|
1.0**. This exception permits the development and distribution of Qt
|
||||||
|
Creator plugins under licenses of the plugin author's own choosing,
|
||||||
|
notwithstanding the GPL's general linking requirements. It is this
|
||||||
|
exception that allows QodeAssist to be offered under both GPL-3.0 and a
|
||||||
|
separate commercial license.
|
||||||
|
|
||||||
|
The original copyright and license notices of The Qt Company are
|
||||||
|
preserved in the relevant source files and must not be removed.
|
||||||
|
|
||||||
|
For Qt Creator's licensing terms, see
|
||||||
|
[LICENSE.GPL3-EXCEPT](https://github.com/qt-creator/qt-creator/blob/master/LICENSES/LICENSE.GPL3-EXCEPT).
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "RefactorSuggestion.hpp"
|
#include "RefactorSuggestion.hpp"
|
||||||
#include "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
#include "RefactorSuggestionHoverHandler.hpp"
|
||||||
#include "RefactorSuggestion.hpp"
|
#include "RefactorSuggestion.hpp"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "FlowEditor.hpp"
|
#include "FlowEditor.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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"
|
#include "FlowItem.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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 "FlowsModel.hpp"
|
||||||
|
|
||||||
#include "FlowManager.hpp"
|
#include "FlowManager.hpp"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#include "GridBackground.hpp"
|
#include "GridBackground.hpp"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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 "TaskConnectionItem.hpp"
|
||||||
#include "TaskItem.hpp"
|
#include "TaskItem.hpp"
|
||||||
#include "TaskPortItem.hpp"
|
#include "TaskPortItem.hpp"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include "TaskConnection.hpp"
|
#include "TaskConnection.hpp"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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"
|
#include "TaskConnectionsModel.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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"
|
#include "TaskItem.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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"
|
#include "TaskModel.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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"
|
#include "TaskPortItem.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// 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
|
#pragma once
|
||||||
|
|
||||||
#include <TaskPort.hpp>
|
#include <TaskPort.hpp>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user