Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a8fbe1792 | ||
|
|
d867a6f0be | ||
|
|
248530c746 | ||
|
|
c73b71f328 | ||
|
|
d2c1e39a2e | ||
|
|
e86e7e103e | ||
|
|
42199024ff | ||
|
|
620fded2e1 | ||
|
|
90b7ed26b1 | ||
|
|
25c4d5f185 | ||
|
|
7a551ed384 | ||
|
|
ca0a47b160 | ||
|
|
6b069b55e3 | ||
|
|
2891b313d2 | ||
|
|
ede2c01eb7 | ||
|
|
6c05f0d594 | ||
|
|
15d714588f | ||
|
|
9a2ba08538 | ||
|
|
37084bec59 | ||
|
|
6910037e97 | ||
|
|
a72cdd85a4 | ||
|
|
31b4e73af5 | ||
|
|
088887c802 | ||
|
|
b7a9787cc3 | ||
|
|
e2e13f0f38 | ||
|
|
49ae335d7d | ||
|
|
2ba58a403f | ||
|
|
3de1619bf0 | ||
|
|
ec45067336 | ||
|
|
52fb65c5b1 | ||
|
|
478f369ad2 | ||
|
|
762c965377 | ||
|
|
d2b93310e2 | ||
|
|
f3b1e7f411 | ||
|
|
a55c6ccfdb | ||
|
|
b32433c336 | ||
|
|
6f11260cd1 | ||
|
|
ddd6aba091 | ||
|
|
e3f464c54e | ||
|
|
e86e58337a | ||
|
|
dbd47387be | ||
|
|
50e1276ab2 | ||
|
|
50c948ccfe | ||
|
|
949dad4fd2 | ||
|
|
01fd7dad6f | ||
|
|
fd408ba415 | ||
|
|
14e7ea2ec3 | ||
|
|
9f050aec67 | ||
|
|
9e118ddfaf | ||
|
|
157498b770 | ||
|
|
5c8a8f305d | ||
|
|
fc33bb60d0 | ||
|
|
498eb4d932 | ||
|
|
fb941cea99 | ||
|
|
a0af983bda | ||
|
|
4bd96e0718 | ||
|
|
7b0d3c2abb | ||
|
|
75d1551b00 | ||
|
|
406ba05bfb | ||
|
|
7a97d0aba5 | ||
|
|
b19c4c0c0c | ||
|
|
a466332822 | ||
|
|
e1fa01d123 | ||
|
|
37e41d3b76 | ||
|
|
2d5667d8ca | ||
|
|
22377c8f6a | ||
|
|
595895840a | ||
|
|
f6d647d5c8 | ||
|
|
1f9c60ffb2 | ||
|
|
6ec4a61c0c | ||
|
|
7feb088de3 | ||
|
|
627a821115 | ||
|
|
9b0ae98f02 | ||
|
|
85a7bba90e | ||
|
|
b18ef4c400 | ||
|
|
bbacdfc22a | ||
|
|
670f81c3dd | ||
|
|
b4f31dee23 | ||
|
|
dc6ec4fb4f | ||
|
|
07de415346 | ||
|
|
a15f64a234 |
16
.github/workflows/build_cmake.yml
vendored
@@ -46,20 +46,18 @@ jobs:
|
|||||||
}
|
}
|
||||||
qt_config:
|
qt_config:
|
||||||
- {
|
- {
|
||||||
qt_version: "6.8.3",
|
qt_version: "6.10.1",
|
||||||
qt_creator_version: "16.0.2"
|
qt_creator_version: "18.0.2"
|
||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
qt_version: "6.9.2",
|
qt_version: "6.10.2",
|
||||||
qt_creator_version: "17.0.2"
|
qt_creator_version: "19.0.0"
|
||||||
}
|
|
||||||
- {
|
|
||||||
qt_version: "6.10.0",
|
|
||||||
qt_creator_version: "18.0.0"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Checkout submodules
|
- name: Checkout submodules
|
||||||
id: git
|
id: git
|
||||||
@@ -165,7 +163,7 @@ jobs:
|
|||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
foreach(package qtbase qtdeclarative qttools)
|
foreach(package qtbase qtdeclarative qttools qtsvg)
|
||||||
downloadAndExtract(
|
downloadAndExtract(
|
||||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||||
${package}.7z
|
${package}.7z
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -78,3 +78,5 @@ CMakeLists.txt.user*
|
|||||||
/.cursor
|
/.cursor
|
||||||
/.vscode
|
/.vscode
|
||||||
.qtc_clangd/compile_commands.json
|
.qtc_clangd/compile_commands.json
|
||||||
|
CLAUDE.md
|
||||||
|
/.claude
|
||||||
|
|||||||
3
.gitmodules
vendored
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "sources/external/llmqore"]
|
||||||
|
path = sources/external/llmqore
|
||||||
|
url = https://github.com/Palm1r/llmqore.git
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network 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)
|
||||||
@@ -34,7 +34,8 @@ add_definitions(
|
|||||||
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(llmcore)
|
add_subdirectory(sources/external/llmqore)
|
||||||
|
add_subdirectory(pluginllmcore)
|
||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
add_subdirectory(logger)
|
add_subdirectory(logger)
|
||||||
add_subdirectory(UIControls)
|
add_subdirectory(UIControls)
|
||||||
@@ -57,9 +58,12 @@ add_qtc_plugin(QodeAssist
|
|||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::Widgets
|
Qt::Widgets
|
||||||
Qt::Network
|
Qt::Network
|
||||||
|
Qt::Svg
|
||||||
QtCreator::ExtensionSystem
|
QtCreator::ExtensionSystem
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::CPlusPlus
|
QtCreator::CPlusPlus
|
||||||
|
LLMQore
|
||||||
|
PluginLLMCore
|
||||||
QodeAssistChatViewplugin
|
QodeAssistChatViewplugin
|
||||||
SOURCES
|
SOURCES
|
||||||
.github/workflows/build_cmake.yml
|
.github/workflows/build_cmake.yml
|
||||||
@@ -69,6 +73,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssistConstants.hpp
|
QodeAssistConstants.hpp
|
||||||
QodeAssisttr.h
|
QodeAssisttr.h
|
||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
|
RefactorContextHelper.hpp
|
||||||
templates/Templates.hpp
|
templates/Templates.hpp
|
||||||
templates/CodeLlamaFim.hpp
|
templates/CodeLlamaFim.hpp
|
||||||
templates/Ollama.hpp
|
templates/Ollama.hpp
|
||||||
@@ -88,17 +93,30 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/GoogleAI.hpp
|
templates/GoogleAI.hpp
|
||||||
templates/LlamaCppFim.hpp
|
templates/LlamaCppFim.hpp
|
||||||
templates/Qwen3CoderFIM.hpp
|
templates/Qwen3CoderFIM.hpp
|
||||||
|
templates/OpenAIResponses.hpp
|
||||||
providers/Providers.hpp
|
providers/Providers.hpp
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||||
|
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
||||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
||||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
||||||
|
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
|
||||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
||||||
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
||||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||||
providers/CodestralProvider.hpp providers/CodestralProvider.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
|
||||||
@@ -121,24 +139,28 @@ add_qtc_plugin(QodeAssist
|
|||||||
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
||||||
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
||||||
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
||||||
|
widgets/RefactorWidget.hpp widgets/RefactorWidget.cpp
|
||||||
|
widgets/RefactorWidgetHandler.hpp widgets/RefactorWidgetHandler.cpp
|
||||||
|
widgets/ContextExtractor.hpp
|
||||||
|
widgets/DiffStatistics.hpp
|
||||||
|
|
||||||
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
||||||
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
tools/ToolsRegistration.hpp tools/ToolsRegistration.cpp
|
||||||
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
|
|
||||||
tools/ToolHandler.hpp tools/ToolHandler.cpp
|
|
||||||
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
|
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
|
||||||
tools/ToolsManager.hpp tools/ToolsManager.cpp
|
|
||||||
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
||||||
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
||||||
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
||||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
||||||
|
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
|
||||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
tools/FindFileTool.hpp tools/FindFileTool.cpp
|
||||||
|
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
|
||||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
tools/TodoTool.hpp tools/TodoTool.cpp
|
||||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
|
||||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
|
||||||
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
|
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
|
||||||
|
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
|
|
||||||
qml/controls/AttachedFilesPlace.qml
|
qml/controls/AttachedFilesPlace.qml
|
||||||
qml/controls/BottomBar.qml
|
qml/controls/BottomBar.qml
|
||||||
|
qml/controls/FileMentionPopup.qml
|
||||||
qml/controls/FileEditsActionBar.qml
|
qml/controls/FileEditsActionBar.qml
|
||||||
qml/controls/RulesViewer.qml
|
qml/controls/ContextViewer.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
|
||||||
@@ -43,12 +44,17 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/chat-icon.svg
|
icons/chat-icon.svg
|
||||||
icons/chat-pause-icon.svg
|
icons/chat-pause-icon.svg
|
||||||
icons/rules-icon.svg
|
icons/rules-icon.svg
|
||||||
|
icons/context-icon.svg
|
||||||
icons/open-in-editor.svg
|
icons/open-in-editor.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
|
||||||
icons/thinking-icon-on.svg
|
icons/thinking-icon-on.svg
|
||||||
icons/thinking-icon-off.svg
|
icons/thinking-icon-off.svg
|
||||||
|
icons/tools-icon-on.svg
|
||||||
|
icons/tools-icon-off.svg
|
||||||
|
icons/settings-icon.svg
|
||||||
|
icons/compress-icon.svg
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
@@ -61,6 +67,9 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
ChatView.hpp ChatView.cpp
|
ChatView.hpp ChatView.cpp
|
||||||
ChatData.hpp
|
ChatData.hpp
|
||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
|
FileMentionItem.hpp FileMentionItem.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
@@ -71,11 +80,12 @@ target_link_libraries(QodeAssistChatView
|
|||||||
Qt::Network
|
Qt::Network
|
||||||
QtCreator::Core
|
QtCreator::Core
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
LLMCore
|
PluginLLMCore
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
Context
|
Context
|
||||||
QodeAssistUIControlsplugin
|
QodeAssistUIControlsplugin
|
||||||
QodeAssistLogger
|
QodeAssistLogger
|
||||||
|
LLMQore
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
|
|||||||
290
ChatView/ChatCompressor.cpp
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "ChatCompressor.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include "ChatModel.hpp"
|
||||||
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "PromptTemplateManager.hpp"
|
||||||
|
#include "ProvidersManager.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatCompressor::ChatCompressor(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
||||||
|
{
|
||||||
|
if (m_isCompressing) {
|
||||||
|
emit compressionFailed(tr("Compression already in progress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatFilePath.isEmpty()) {
|
||||||
|
emit compressionFailed(tr("No chat file to compress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chatModel || chatModel->rowCount() == 0) {
|
||||||
|
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (!m_provider) {
|
||||||
|
emit compressionFailed(tr("No provider available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto templateName = Settings::generalSettings().caTemplate();
|
||||||
|
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||||
|
templateName);
|
||||||
|
|
||||||
|
if (!promptTemplate) {
|
||||||
|
emit compressionFailed(tr("No template available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_isCompressing = true;
|
||||||
|
m_chatModel = chatModel;
|
||||||
|
m_originalChatPath = chatFilePath;
|
||||||
|
m_accumulatedSummary.clear();
|
||||||
|
|
||||||
|
emit compressionStarted();
|
||||||
|
|
||||||
|
connectProviderSignals();
|
||||||
|
|
||||||
|
QJsonObject payload{
|
||||||
|
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||||
|
|
||||||
|
buildRequestPayload(payload, promptTemplate);
|
||||||
|
|
||||||
|
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||||
|
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||||
|
: promptTemplate->endpoint();
|
||||||
|
m_currentRequestId = m_provider->sendRequest(
|
||||||
|
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||||
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatCompressor::isCompressing() const
|
||||||
|
{
|
||||||
|
return m_isCompressing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::cancelCompression()
|
||||||
|
{
|
||||||
|
if (!m_isCompressing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE("Cancelling compression request");
|
||||||
|
|
||||||
|
if (m_provider && !m_currentRequestId.isEmpty())
|
||||||
|
m_provider->cancelRequest(m_currentRequestId);
|
||||||
|
|
||||||
|
cleanupState();
|
||||||
|
emit compressionFailed(tr("Compression cancelled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
||||||
|
{
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_accumulatedSummary += partialText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
||||||
|
{
|
||||||
|
Q_UNUSED(fullText)
|
||||||
|
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
||||||
|
|
||||||
|
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||||
|
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
||||||
|
handleCompressionError(tr("Failed to save compressed chat"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||||
|
cleanupState();
|
||||||
|
emit compressionCompleted(compressedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
||||||
|
{
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Compression request failed: %1").arg(error));
|
||||||
|
handleCompressionError(tr("Compression failed: %1").arg(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::handleCompressionError(const QString &error)
|
||||||
|
{
|
||||||
|
cleanupState();
|
||||||
|
emit compressionFailed(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatCompressor::createCompressedChatPath(const QString &originalPath) const
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(originalPath);
|
||||||
|
QString hash = QString::number(QDateTime::currentMSecsSinceEpoch() % 100000, 16);
|
||||||
|
return QString("%1/%2_%3.%4")
|
||||||
|
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatCompressor::buildCompressionPrompt() const
|
||||||
|
{
|
||||||
|
return QStringLiteral(
|
||||||
|
"Please create a comprehensive summary of our entire conversation above. "
|
||||||
|
"The summary should:\n"
|
||||||
|
"1. Preserve all important context, decisions, and key information\n"
|
||||||
|
"2. Maintain technical details, code snippets, file references, and specific examples\n"
|
||||||
|
"3. Keep the chronological flow of the discussion\n"
|
||||||
|
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
|
||||||
|
"5. Be written in clear, structured format\n"
|
||||||
|
"6. Use markdown formatting for better readability\n\n"
|
||||||
|
"Create the summary now:");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::buildRequestPayload(
|
||||||
|
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
||||||
|
{
|
||||||
|
PluginLLMCore::ContextData context;
|
||||||
|
|
||||||
|
context.systemPrompt = QStringLiteral(
|
||||||
|
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||||
|
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||||
|
|
||||||
|
QVector<PluginLLMCore::Message> messages;
|
||||||
|
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||||
|
if (msg.role == ChatModel::ChatRole::Tool
|
||||||
|
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||||
|
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
PluginLLMCore::Message apiMessage;
|
||||||
|
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||||
|
apiMessage.content = msg.content;
|
||||||
|
messages.append(apiMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginLLMCore::Message compressionRequest;
|
||||||
|
compressionRequest.role = "user";
|
||||||
|
compressionRequest.content = buildCompressionPrompt();
|
||||||
|
messages.append(compressionRequest);
|
||||||
|
|
||||||
|
context.history = messages;
|
||||||
|
|
||||||
|
m_provider->prepareRequest(
|
||||||
|
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatCompressor::createCompressedChatFile(
|
||||||
|
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||||
|
{
|
||||||
|
QFile sourceFile(sourcePath);
|
||||||
|
if (!sourceFile.open(QIODevice::ReadOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
|
||||||
|
sourceFile.close();
|
||||||
|
|
||||||
|
if (doc.isNull() || !doc.isObject()) {
|
||||||
|
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
|
||||||
|
.arg(sourcePath, parseError.errorString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root = doc.object();
|
||||||
|
|
||||||
|
QJsonObject summaryMessage;
|
||||||
|
summaryMessage["role"] = "assistant";
|
||||||
|
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||||
|
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
summaryMessage["isRedacted"] = false;
|
||||||
|
summaryMessage["attachments"] = QJsonArray();
|
||||||
|
summaryMessage["images"] = QJsonArray();
|
||||||
|
|
||||||
|
root["messages"] = QJsonArray{summaryMessage};
|
||||||
|
|
||||||
|
if (QFile::exists(destPath))
|
||||||
|
QFile::remove(destPath);
|
||||||
|
|
||||||
|
QFile destFile(destPath);
|
||||||
|
if (!destFile.open(QIODevice::WriteOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create compressed chat file: %1").arg(destPath));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::connectProviderSignals()
|
||||||
|
{
|
||||||
|
auto *c = m_provider->client();
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
c,
|
||||||
|
&::LLMQore::BaseClient::chunkReceived,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onPartialResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
c,
|
||||||
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onFullResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
c,
|
||||||
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onRequestFailed,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::disconnectAllSignals()
|
||||||
|
{
|
||||||
|
for (const auto &connection : std::as_const(m_connections))
|
||||||
|
disconnect(connection);
|
||||||
|
m_connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::cleanupState()
|
||||||
|
{
|
||||||
|
disconnectAllSignals();
|
||||||
|
|
||||||
|
m_isCompressing = false;
|
||||||
|
m_currentRequestId.clear();
|
||||||
|
m_originalChatPath.clear();
|
||||||
|
m_accumulatedSummary.clear();
|
||||||
|
m_chatModel = nullptr;
|
||||||
|
m_provider = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
63
ChatView/ChatCompressor.hpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::PluginLLMCore {
|
||||||
|
class Provider;
|
||||||
|
class PromptTemplate;
|
||||||
|
} // namespace QodeAssist::PluginLLMCore
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatModel;
|
||||||
|
|
||||||
|
class ChatCompressor : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatCompressor(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
||||||
|
|
||||||
|
bool isCompressing() const;
|
||||||
|
void cancelCompression();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void compressionStarted();
|
||||||
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
||||||
|
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
||||||
|
void onRequestFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString createCompressedChatPath(const QString &originalPath) const;
|
||||||
|
QString buildCompressionPrompt() const;
|
||||||
|
bool createCompressedChatFile(
|
||||||
|
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||||
|
void connectProviderSignals();
|
||||||
|
void disconnectAllSignals();
|
||||||
|
void cleanupState();
|
||||||
|
void handleCompressionError(const QString &error);
|
||||||
|
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
||||||
|
|
||||||
|
bool m_isCompressing = false;
|
||||||
|
QString m_currentRequestId;
|
||||||
|
QString m_originalChatPath;
|
||||||
|
QString m_accumulatedSummary;
|
||||||
|
PluginLLMCore::Provider *m_provider = nullptr;
|
||||||
|
ChatModel *m_chatModel = nullptr;
|
||||||
|
|
||||||
|
QList<QMetaObject::Connection> m_connections;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
190
ChatView/ChatFileManager.cpp
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "ChatFileManager.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatFileManager::ChatFileManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_intermediateStorageDir(getIntermediateStorageDir())
|
||||||
|
{}
|
||||||
|
|
||||||
|
ChatFileManager::~ChatFileManager() = default;
|
||||||
|
|
||||||
|
QStringList ChatFileManager::processDroppedFiles(const QStringList &filePaths)
|
||||||
|
{
|
||||||
|
QStringList processedPaths;
|
||||||
|
processedPaths.reserve(filePaths.size());
|
||||||
|
|
||||||
|
for (const QString &filePath : filePaths) {
|
||||||
|
if (!isFileAccessible(filePath)) {
|
||||||
|
const QString error = tr("File is not accessible: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString copiedPath = copyToIntermediateStorage(filePath);
|
||||||
|
if (!copiedPath.isEmpty()) {
|
||||||
|
processedPaths.append(copiedPath);
|
||||||
|
emit fileCopiedToStorage(filePath, copiedPath);
|
||||||
|
LOG_MESSAGE(QString("File copied to storage: %1 -> %2").arg(filePath, copiedPath));
|
||||||
|
} else {
|
||||||
|
const QString error = tr("Failed to copy file: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::setChatFilePath(const QString &chatFilePath)
|
||||||
|
{
|
||||||
|
m_chatFilePath = chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::chatFilePath() const
|
||||||
|
{
|
||||||
|
return m_chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::clearIntermediateStorage()
|
||||||
|
{
|
||||||
|
QDir dir(m_intermediateStorageDir);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
LOG_MESSAGE(QString("Removed intermediate file: %1").arg(fileInfo.fileName()));
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("Failed to remove intermediate file: %1")
|
||||||
|
.arg(fileInfo.fileName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatFileManager::isFileAccessible(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
return fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::cleanupGlobalIntermediateStorage()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir(intermediatePath);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
int removedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
removedCount++;
|
||||||
|
} else {
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0 || failedCount > 0) {
|
||||||
|
LOG_MESSAGE(QString("ChatFileManager global cleanup: removed=%1, failed=%2")
|
||||||
|
.arg(removedCount)
|
||||||
|
.arg(failedCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::copyToIntermediateStorage(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
if (!fileInfo.exists() || !fileInfo.isFile()) {
|
||||||
|
LOG_MESSAGE(QString("Source file does not exist or is not a file: %1").arg(filePath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo.size() == 0) {
|
||||||
|
LOG_MESSAGE(QString("Source file is empty: %1").arg(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString newFileName = generateIntermediateFileName(filePath);
|
||||||
|
const QString destinationPath = QDir(m_intermediateStorageDir).filePath(newFileName);
|
||||||
|
|
||||||
|
if (QFileInfo::exists(destinationPath)) {
|
||||||
|
QFile::remove(destinationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QFile::copy(filePath, destinationPath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to copy file: %1 -> %2").arg(filePath, destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile copiedFile(destinationPath);
|
||||||
|
if (!copiedFile.exists()) {
|
||||||
|
LOG_MESSAGE(QString("Copied file does not exist after copy: %1").arg(destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
copiedFile.setPermissions(QFile::ReadUser | QFile::WriteUser);
|
||||||
|
|
||||||
|
return destinationPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::getIntermediateStorageDir()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir;
|
||||||
|
if (!dir.exists(intermediatePath) && !dir.mkpath(intermediatePath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create intermediate storage directory: %1")
|
||||||
|
.arg(intermediatePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return intermediatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::generateIntermediateFileName(const QString &originalPath)
|
||||||
|
{
|
||||||
|
const QFileInfo fileInfo(originalPath);
|
||||||
|
const QString extension = fileInfo.suffix();
|
||||||
|
QString baseName = fileInfo.completeBaseName().left(30);
|
||||||
|
|
||||||
|
static const QRegularExpression specialChars("[^a-zA-Z0-9_-]");
|
||||||
|
baseName.replace(specialChars, "_");
|
||||||
|
|
||||||
|
if (baseName.isEmpty()) {
|
||||||
|
baseName = "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
|
||||||
|
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces).left(8);
|
||||||
|
|
||||||
|
return QString("%1_%2_%3.%4").arg(baseName, timestamp, uuid, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
43
ChatView/ChatFileManager.hpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QMap>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatFileManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatFileManager(QObject *parent = nullptr);
|
||||||
|
~ChatFileManager();
|
||||||
|
|
||||||
|
QStringList processDroppedFiles(const QStringList &filePaths);
|
||||||
|
void setChatFilePath(const QString &chatFilePath);
|
||||||
|
QString chatFilePath() const;
|
||||||
|
void clearIntermediateStorage();
|
||||||
|
|
||||||
|
static bool isFileAccessible(const QString &filePath);
|
||||||
|
static void cleanupGlobalIntermediateStorage();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileOperationFailed(const QString &error);
|
||||||
|
void fileCopiedToStorage(const QString &originalPath, const QString &newPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString copyToIntermediateStorage(const QString &filePath);
|
||||||
|
QString getIntermediateStorageDir();
|
||||||
|
QString generateIntermediateFileName(const QString &originalPath);
|
||||||
|
|
||||||
|
QString m_chatFilePath;
|
||||||
|
QString m_intermediateStorageDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include <utils/aspects.h>
|
#include <utils/aspects.h>
|
||||||
@@ -78,11 +62,26 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
return message.content;
|
return message.content;
|
||||||
}
|
}
|
||||||
case Roles::Attachments: {
|
case Roles::Attachments: {
|
||||||
QStringList filenames;
|
QVariantList attachmentsList;
|
||||||
for (const auto &attachment : message.attachments) {
|
for (const auto &attachment : message.attachments) {
|
||||||
filenames << attachment.filename;
|
QVariantMap attachmentMap;
|
||||||
|
attachmentMap["fileName"] = attachment.filename;
|
||||||
|
attachmentMap["storedPath"] = attachment.content;
|
||||||
|
|
||||||
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
|
QFileInfo fileInfo(m_chatFilePath);
|
||||||
|
QString baseName = fileInfo.completeBaseName();
|
||||||
|
QString dirPath = fileInfo.absolutePath();
|
||||||
|
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||||
|
QString fullPath = QDir(contentFolder).filePath(attachment.content);
|
||||||
|
attachmentMap["filePath"] = fullPath;
|
||||||
|
} else {
|
||||||
|
attachmentMap["filePath"] = QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsList.append(attachmentMap);
|
||||||
}
|
}
|
||||||
return filenames;
|
return attachmentsList;
|
||||||
}
|
}
|
||||||
case Roles::IsRedacted: {
|
case Roles::IsRedacted: {
|
||||||
return message.isRedacted;
|
return message.isRedacted;
|
||||||
@@ -95,16 +94,17 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
imageMap["storedPath"] = image.storedPath;
|
imageMap["storedPath"] = image.storedPath;
|
||||||
imageMap["mediaType"] = image.mediaType;
|
imageMap["mediaType"] = image.mediaType;
|
||||||
|
|
||||||
// Generate proper file URL for cross-platform compatibility
|
|
||||||
if (!m_chatFilePath.isEmpty()) {
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
QFileInfo fileInfo(m_chatFilePath);
|
QFileInfo fileInfo(m_chatFilePath);
|
||||||
QString baseName = fileInfo.completeBaseName();
|
QString baseName = fileInfo.completeBaseName();
|
||||||
QString dirPath = fileInfo.absolutePath();
|
QString dirPath = fileInfo.absolutePath();
|
||||||
QString imagesFolder = QDir(dirPath).filePath(baseName + "_images");
|
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||||
QString fullPath = QDir(imagesFolder).filePath(image.storedPath);
|
QString fullPath = QDir(contentFolder).filePath(image.storedPath);
|
||||||
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
||||||
|
imageMap["filePath"] = fullPath;
|
||||||
} else {
|
} else {
|
||||||
imageMap["imageUrl"] = QString();
|
imageMap["imageUrl"] = QString();
|
||||||
|
imageMap["filePath"] = QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
imagesList.append(imageMap);
|
imagesList.append(imageMap);
|
||||||
@@ -132,29 +132,26 @@ void ChatModel::addMessage(
|
|||||||
ChatRole role,
|
ChatRole role,
|
||||||
const QString &id,
|
const QString &id,
|
||||||
const QList<Context::ContentFile> &attachments,
|
const QList<Context::ContentFile> &attachments,
|
||||||
const QList<ImageAttachment> &images)
|
const QList<ImageAttachment> &images,
|
||||||
|
bool isRedacted,
|
||||||
|
const QString &signature)
|
||||||
{
|
{
|
||||||
QString fullContent = content;
|
|
||||||
if (!attachments.isEmpty()) {
|
|
||||||
fullContent += "\n\nAttached files list:";
|
|
||||||
for (const auto &attachment : attachments) {
|
|
||||||
fullContent += QString("\nname: %1\nfile content:\n%2")
|
|
||||||
.arg(attachment.filename, attachment.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
||||||
&& m_messages.last().role == role) {
|
&& m_messages.last().role == role) {
|
||||||
Message &lastMessage = m_messages.last();
|
Message &lastMessage = m_messages.last();
|
||||||
lastMessage.content = content;
|
lastMessage.content = content;
|
||||||
lastMessage.attachments = attachments;
|
lastMessage.attachments = attachments;
|
||||||
lastMessage.images = images;
|
lastMessage.images = images;
|
||||||
|
lastMessage.isRedacted = isRedacted;
|
||||||
|
lastMessage.signature = signature;
|
||||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||||
} else {
|
} else {
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||||
Message newMessage{role, content, id};
|
Message newMessage{role, content, id};
|
||||||
newMessage.attachments = attachments;
|
newMessage.attachments = attachments;
|
||||||
newMessage.images = images;
|
newMessage.images = images;
|
||||||
|
newMessage.isRedacted = isRedacted;
|
||||||
|
newMessage.signature = signature;
|
||||||
m_messages.append(newMessage);
|
m_messages.append(newMessage);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|
||||||
@@ -450,6 +447,16 @@ void ChatModel::addThinkingBlock(
|
|||||||
displayContent += "\n[Signature: " + signature.left(40) + "...]";
|
displayContent += "\n[Signature: " + signature.left(40) + "...]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_messages.size(); ++i) {
|
||||||
|
if (m_messages[i].role == ChatRole::Thinking && m_messages[i].id == requestId) {
|
||||||
|
m_messages[i].content = displayContent;
|
||||||
|
m_messages[i].signature = signature;
|
||||||
|
emit dataChanged(index(i), index(i));
|
||||||
|
LOG_MESSAGE(QString("Updated existing thinking message at index %1").arg(i));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||||
Message thinkingMessage;
|
Message thinkingMessage;
|
||||||
thinkingMessage.role = ChatRole::Thinking;
|
thinkingMessage.role = ChatRole::Thinking;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -73,7 +57,9 @@ public:
|
|||||||
ChatRole role,
|
ChatRole role,
|
||||||
const QString &id,
|
const QString &id,
|
||||||
const QList<Context::ContentFile> &attachments = {},
|
const QList<Context::ContentFile> &attachments = {},
|
||||||
const QList<ImageAttachment> &images = {});
|
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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,21 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
|
#include "ChatFileManager.hpp"
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
#include "llmcore/PromptProviderChat.hpp"
|
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatCompressor;
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
class ChatRootView : public QQuickItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -48,10 +36,8 @@ class ChatRootView : public QQuickItem
|
|||||||
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(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
||||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||||
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
|
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
||||||
Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL)
|
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
||||||
Q_PROPERTY(
|
|
||||||
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged 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)
|
||||||
@@ -60,6 +46,12 @@ class ChatRootView : public QQuickItem
|
|||||||
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 availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
||||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
||||||
|
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
@@ -95,6 +87,9 @@ public:
|
|||||||
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 openRulesFolder();
|
||||||
|
Q_INVOKABLE void openSettings();
|
||||||
|
|
||||||
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
|
|
||||||
Q_INVOKABLE void updateInputTokensCount();
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
@@ -127,11 +122,10 @@ public:
|
|||||||
Q_INVOKABLE QString getRuleContent(int index);
|
Q_INVOKABLE QString getRuleContent(int index);
|
||||||
Q_INVOKABLE void refreshRules();
|
Q_INVOKABLE void refreshRules();
|
||||||
|
|
||||||
bool isAgentMode() const;
|
bool useTools() const;
|
||||||
void setIsAgentMode(bool newIsAgentMode);
|
void setUseTools(bool enabled);
|
||||||
bool isThinkingMode() const;
|
bool useThinking() const;
|
||||||
void setIsThinkingMode(bool newIsThinkingMode);
|
void setUseThinking(bool enabled);
|
||||||
bool toolsSupportEnabled() const;
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -146,6 +140,18 @@ public:
|
|||||||
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
||||||
QStringList availableConfigurations() const;
|
QStringList availableConfigurations() const;
|
||||||
QString currentConfiguration() const;
|
QString currentConfiguration() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void compressCurrentChat();
|
||||||
|
Q_INVOKABLE void cancelCompression();
|
||||||
|
|
||||||
|
Q_INVOKABLE void loadAvailableAgentRoles();
|
||||||
|
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
||||||
|
Q_INVOKABLE void openAgentRolesSettings();
|
||||||
|
QStringList availableAgentRoles() const;
|
||||||
|
QString currentAgentRole() const;
|
||||||
|
QString baseSystemPrompt() const;
|
||||||
|
QString currentAgentRoleDescription() const;
|
||||||
|
QString currentAgentRoleSystemPrompt() const;
|
||||||
|
|
||||||
int currentMessageTotalEdits() const;
|
int currentMessageTotalEdits() const;
|
||||||
int currentMessageAppliedEdits() const;
|
int currentMessageAppliedEdits() const;
|
||||||
@@ -155,6 +161,8 @@ public:
|
|||||||
QString lastInfoMessage() const;
|
QString lastInfoMessage() const;
|
||||||
|
|
||||||
bool isThinkingSupport() const;
|
bool isThinkingSupport() const;
|
||||||
|
|
||||||
|
bool isCompressing() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
@@ -162,6 +170,7 @@ public slots:
|
|||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
void clearAttachmentFiles();
|
void clearAttachmentFiles();
|
||||||
void clearLinkedFiles();
|
void clearLinkedFiles();
|
||||||
|
void clearMessages();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void chatModelChanged();
|
void chatModelChanged();
|
||||||
@@ -184,15 +193,24 @@ signals:
|
|||||||
void activeRulesChanged();
|
void activeRulesChanged();
|
||||||
void activeRulesCountChanged();
|
void activeRulesCountChanged();
|
||||||
|
|
||||||
void isAgentModeChanged();
|
void useToolsChanged();
|
||||||
void isThinkingModeChanged();
|
void useThinkingChanged();
|
||||||
void toolsSupportEnabledChanged();
|
|
||||||
void currentMessageEditsStatsChanged();
|
void currentMessageEditsStatsChanged();
|
||||||
|
|
||||||
void isThinkingSupportChanged();
|
void isThinkingSupportChanged();
|
||||||
void availableConfigurationsChanged();
|
void availableConfigurationsChanged();
|
||||||
void currentConfigurationChanged();
|
void currentConfigurationChanged();
|
||||||
|
|
||||||
|
void availableAgentRolesChanged();
|
||||||
|
void currentAgentRoleChanged();
|
||||||
|
void baseSystemPromptChanged();
|
||||||
|
|
||||||
|
void isCompressingChanged();
|
||||||
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
void openFilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
@@ -201,8 +219,9 @@ private:
|
|||||||
bool hasImageAttachments(const QStringList &attachments) const;
|
bool hasImageAttachments(const QStringList &attachments) const;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
LLMCore::PromptProviderChat m_promptProvider;
|
PluginLLMCore::PromptProviderChat m_promptProvider;
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
|
ChatFileManager *m_fileManager;
|
||||||
QString m_currentTemplate;
|
QString m_currentTemplate;
|
||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
@@ -214,8 +233,6 @@ private:
|
|||||||
bool m_isRequestInProgress;
|
bool m_isRequestInProgress;
|
||||||
QString m_lastErrorMessage;
|
QString m_lastErrorMessage;
|
||||||
QVariantList m_activeRules;
|
QVariantList m_activeRules;
|
||||||
bool m_isAgentMode;
|
|
||||||
bool m_isThinkingMode;
|
|
||||||
|
|
||||||
QString m_currentMessageRequestId;
|
QString m_currentMessageRequestId;
|
||||||
int m_currentMessageTotalEdits{0};
|
int m_currentMessageTotalEdits{0};
|
||||||
@@ -226,6 +243,11 @@ private:
|
|||||||
|
|
||||||
QStringList m_availableConfigurations;
|
QStringList m_availableConfigurations;
|
||||||
QString m_currentConfiguration;
|
QString m_currentConfiguration;
|
||||||
|
|
||||||
|
QStringList m_availableAgentRoles;
|
||||||
|
QString m_currentAgentRole;
|
||||||
|
|
||||||
|
ChatCompressor *m_chatCompressor;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
@@ -30,7 +14,7 @@
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
const QString ChatSerializer::VERSION = "0.1";
|
const QString ChatSerializer::VERSION = "0.2";
|
||||||
|
|
||||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
||||||
{
|
{
|
||||||
@@ -38,14 +22,6 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {false, "Failed to create directory structure"};
|
return {false, "Failed to create directory structure"};
|
||||||
}
|
}
|
||||||
|
|
||||||
QString imagesFolder = getChatImagesFolder(filePath);
|
|
||||||
QDir dir;
|
|
||||||
if (!dir.exists(imagesFolder)) {
|
|
||||||
if (!dir.mkpath(imagesFolder)) {
|
|
||||||
LOG_MESSAGE(QString("Warning: Failed to create images folder: %1").arg(imagesFolder));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::WriteOnly)) {
|
if (!file.open(QIODevice::WriteOnly)) {
|
||||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||||
@@ -88,17 +64,33 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
|||||||
return {true, QString()};
|
return {true, QString()};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, const QString &chatFilePath)
|
QJsonObject ChatSerializer::serializeMessage(
|
||||||
|
const ChatModel::Message &message, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QJsonObject messageObj;
|
QJsonObject messageObj;
|
||||||
messageObj["role"] = static_cast<int>(message.role);
|
messageObj["role"] = static_cast<int>(message.role);
|
||||||
messageObj["content"] = message.content;
|
messageObj["content"] = message.content;
|
||||||
messageObj["id"] = message.id;
|
messageObj["id"] = message.id;
|
||||||
messageObj["isRedacted"] = message.isRedacted;
|
|
||||||
|
if (message.isRedacted) {
|
||||||
|
messageObj["isRedacted"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!message.signature.isEmpty()) {
|
if (!message.signature.isEmpty()) {
|
||||||
messageObj["signature"] = message.signature;
|
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()) {
|
if (!message.images.isEmpty()) {
|
||||||
QJsonArray imagesArray;
|
QJsonArray imagesArray;
|
||||||
for (const auto &image : message.images) {
|
for (const auto &image : message.images) {
|
||||||
@@ -110,11 +102,12 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
|
|||||||
}
|
}
|
||||||
messageObj["images"] = imagesArray;
|
messageObj["images"] = imagesArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
return messageObj;
|
return messageObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, const QString &chatFilePath)
|
ChatModel::Message ChatSerializer::deserializeMessage(
|
||||||
|
const QJsonObject &json, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
ChatModel::Message message;
|
ChatModel::Message message;
|
||||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||||
@@ -122,7 +115,18 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
|||||||
message.id = json["id"].toString();
|
message.id = json["id"].toString();
|
||||||
message.isRedacted = json["isRedacted"].toBool(false);
|
message.isRedacted = json["isRedacted"].toBool(false);
|
||||||
message.signature = json["signature"].toString();
|
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")) {
|
if (json.contains("images")) {
|
||||||
QJsonArray imagesArray = json["images"].toArray();
|
QJsonArray imagesArray = json["images"].toArray();
|
||||||
for (const auto &imageValue : imagesArray) {
|
for (const auto &imageValue : imagesArray) {
|
||||||
@@ -134,7 +138,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
|||||||
message.images.append(image);
|
message.images.append(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +156,8 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
bool ChatSerializer::deserializeChat(
|
||||||
|
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray = json["messages"].toArray();
|
QJsonArray messagesArray = json["messages"].toArray();
|
||||||
QVector<ChatModel::Message> messages;
|
QVector<ChatModel::Message> messages;
|
||||||
@@ -163,14 +168,24 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
|
|||||||
}
|
}
|
||||||
|
|
||||||
model->clear();
|
model->clear();
|
||||||
|
|
||||||
model->setLoadingFromHistory(true);
|
model->setLoadingFromHistory(true);
|
||||||
|
|
||||||
for (const auto &message : messages) {
|
for (const auto &message : messages) {
|
||||||
model->addMessage(message.content, message.role, message.id, message.attachments, message.images);
|
model->addMessage(
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s)").arg(message.images.size()));
|
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);
|
model->setLoadingFromHistory(false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -185,76 +200,88 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatSerializer::validateVersion(const QString &version)
|
bool ChatSerializer::validateVersion(const QString &version)
|
||||||
{
|
{
|
||||||
return version == VERSION;
|
if (version == VERSION) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version == "0.1") {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
||||||
|
"to _content");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::getChatImagesFolder(const QString &chatFilePath)
|
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QFileInfo fileInfo(chatFilePath);
|
QFileInfo fileInfo(chatFilePath);
|
||||||
QString baseName = fileInfo.completeBaseName();
|
QString baseName = fileInfo.completeBaseName();
|
||||||
QString dirPath = fileInfo.absolutePath();
|
QString dirPath = fileInfo.absolutePath();
|
||||||
return QDir(dirPath).filePath(baseName + "_images");
|
return QDir(dirPath).filePath(baseName + "_content");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::saveImageToStorage(const QString &chatFilePath,
|
bool ChatSerializer::saveContentToStorage(
|
||||||
const QString &fileName,
|
const QString &chatFilePath,
|
||||||
const QString &base64Data,
|
const QString &fileName,
|
||||||
QString &storedPath)
|
const QString &base64Data,
|
||||||
|
QString &storedPath)
|
||||||
{
|
{
|
||||||
QString imagesFolder = getChatImagesFolder(chatFilePath);
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
QDir dir;
|
QDir dir;
|
||||||
if (!dir.exists(imagesFolder)) {
|
if (!dir.exists(contentFolder)) {
|
||||||
if (!dir.mkpath(imagesFolder)) {
|
if (!dir.mkpath(contentFolder)) {
|
||||||
LOG_MESSAGE(QString("Failed to create images folder: %1").arg(imagesFolder));
|
LOG_MESSAGE(QString("Failed to create content folder: %1").arg(contentFolder));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QFileInfo originalFileInfo(fileName);
|
QFileInfo originalFileInfo(fileName);
|
||||||
QString extension = originalFileInfo.suffix();
|
QString extension = originalFileInfo.suffix();
|
||||||
QString baseName = originalFileInfo.completeBaseName();
|
QString baseName = originalFileInfo.completeBaseName();
|
||||||
QString uniqueName = QString("%1_%2.%3")
|
QString uniqueName = QString("%1_%2.%3")
|
||||||
.arg(baseName)
|
.arg(baseName)
|
||||||
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
|
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
|
||||||
.arg(extension);
|
.arg(extension);
|
||||||
|
|
||||||
QString fullPath = QDir(imagesFolder).filePath(uniqueName);
|
QString fullPath = QDir(contentFolder).filePath(uniqueName);
|
||||||
|
|
||||||
QByteArray imageData = QByteArray::fromBase64(base64Data.toUtf8());
|
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
|
||||||
QFile file(fullPath);
|
QFile file(fullPath);
|
||||||
if (!file.open(QIODevice::WriteOnly)) {
|
if (!file.open(QIODevice::WriteOnly)) {
|
||||||
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
|
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.write(imageData) == -1) {
|
if (file.write(contentData) == -1) {
|
||||||
LOG_MESSAGE(QString("Failed to write image data: %1").arg(file.errorString()));
|
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
storedPath = uniqueName;
|
storedPath = uniqueName;
|
||||||
LOG_MESSAGE(QString("Saved image: %1 to %2").arg(fileName, fullPath));
|
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::loadImageFromStorage(const QString &chatFilePath, const QString &storedPath)
|
QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath)
|
||||||
{
|
{
|
||||||
QString imagesFolder = getChatImagesFolder(chatFilePath);
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
QString fullPath = QDir(imagesFolder).filePath(storedPath);
|
QString fullPath = QDir(contentFolder).filePath(storedPath);
|
||||||
|
|
||||||
QFile file(fullPath);
|
QFile file(fullPath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
LOG_MESSAGE(QString("Failed to open image file: %1").arg(fullPath));
|
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray imageData = file.readAll();
|
QByteArray contentData = file.readAll();
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
return imageData.toBase64();
|
return contentData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -45,13 +29,13 @@ public:
|
|||||||
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
||||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
||||||
|
|
||||||
// Image management
|
// Content management (images and text files)
|
||||||
static QString getChatImagesFolder(const QString &chatFilePath);
|
static QString getChatContentFolder(const QString &chatFilePath);
|
||||||
static bool saveImageToStorage(const QString &chatFilePath,
|
static bool saveContentToStorage(const QString &chatFilePath,
|
||||||
const QString &fileName,
|
const QString &fileName,
|
||||||
const QString &base64Data,
|
const QString &base64Data,
|
||||||
QString &storedPath);
|
QString &storedPath);
|
||||||
static QString loadImageFromStorage(const QString &chatFilePath, const QString &storedPath);
|
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString VERSION;
|
static const QString VERSION;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ChatUtils.h"
|
#include "ChatUtils.h"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ChatView.hpp"
|
#include "ChatView.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ChatWidget.hpp"
|
#include "ChatWidget.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,12 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
|
||||||
|
#include <projectexplorer/buildconfiguration.h>
|
||||||
|
#include <projectexplorer/target.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -38,20 +26,23 @@
|
|||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
|
|
||||||
|
#include "tools/TodoTool.hpp"
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "ToolsSettings.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
#include "RequestConfig.hpp"
|
#include "ToolsSettings.hpp"
|
||||||
#include <context/ChangesManager.h>
|
|
||||||
#include <RulesLoader.hpp>
|
#include <RulesLoader.hpp>
|
||||||
|
#include <context/ChangesManager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ClientInterface::ClientInterface(
|
ClientInterface::ClientInterface(
|
||||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
|
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
, m_promptProvider(promptProvider)
|
, m_promptProvider(promptProvider)
|
||||||
@@ -67,16 +58,17 @@ 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 useAgentMode)
|
bool useTools,
|
||||||
|
bool useThinking)
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
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);
|
||||||
@@ -85,8 +77,25 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto attachFiles = m_contextManager->getContentFiles(textFiles);
|
QList<Context::ContentFile> storedAttachments;
|
||||||
|
if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
|
auto attachFiles = m_contextManager->getContentFiles(textFiles);
|
||||||
|
for (const auto &file : attachFiles) {
|
||||||
|
QString storedPath;
|
||||||
|
if (ChatSerializer::saveContentToStorage(
|
||||||
|
m_chatFilePath, file.filename, file.content.toUtf8().toBase64(), storedPath)) {
|
||||||
|
Context::ContentFile storedFile;
|
||||||
|
storedFile.filename = file.filename;
|
||||||
|
storedFile.content = storedPath;
|
||||||
|
storedAttachments.append(storedFile);
|
||||||
|
LOG_MESSAGE(QString("Stored text file %1 as %2").arg(file.filename, storedPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!textFiles.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 text file(s)")
|
||||||
|
.arg(textFiles.size()));
|
||||||
|
}
|
||||||
|
|
||||||
QList<ChatModel::ImageAttachment> imageAttachments;
|
QList<ChatModel::ImageAttachment> imageAttachments;
|
||||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
for (const QString &imagePath : imageFiles) {
|
for (const QString &imagePath : imageFiles) {
|
||||||
@@ -94,29 +103,31 @@ void ClientInterface::sendMessage(
|
|||||||
if (base64Data.isEmpty()) {
|
if (base64Data.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString storedPath;
|
QString storedPath;
|
||||||
QFileInfo fileInfo(imagePath);
|
QFileInfo fileInfo(imagePath);
|
||||||
if (ChatSerializer::saveImageToStorage(m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
if (ChatSerializer::saveContentToStorage(
|
||||||
|
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
||||||
ChatModel::ImageAttachment imageAttachment;
|
ChatModel::ImageAttachment imageAttachment;
|
||||||
imageAttachment.fileName = fileInfo.fileName();
|
imageAttachment.fileName = fileInfo.fileName();
|
||||||
imageAttachment.storedPath = storedPath;
|
imageAttachment.storedPath = storedPath;
|
||||||
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
||||||
imageAttachments.append(imageAttachment);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!imageFiles.isEmpty()) {
|
} else if (!imageFiles.isEmpty()) {
|
||||||
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)").arg(imageFiles.size()));
|
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)")
|
||||||
|
.arg(imageFiles.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles, imageAttachments);
|
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
||||||
|
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
auto providerName = Settings::generalSettings().caProvider();
|
||||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||||
@@ -131,21 +142,36 @@ void ClientInterface::sendMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData context;
|
PluginLLMCore::ContextData context;
|
||||||
|
|
||||||
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode;
|
const bool isToolsEnabled = useTools;
|
||||||
|
|
||||||
if (chatAssistantSettings.useSystemPrompt()) {
|
if (chatAssistantSettings.useSystemPrompt()) {
|
||||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
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();
|
||||||
|
|
||||||
if (project) {
|
if (project) {
|
||||||
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
||||||
systemPrompt += QString("\n# Active Project path: %1").arg(project->projectDirectory().toUrlishString());
|
systemPrompt += QString("\n# Active Project path: %1")
|
||||||
|
.arg(project->projectDirectory().toUrlishString());
|
||||||
|
|
||||||
|
if (auto target = project->activeTarget()) {
|
||||||
|
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||||
|
systemPrompt += QString("\n# Active Build directory: %1")
|
||||||
|
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString projectRules
|
QString projectRules
|
||||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
|
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
if (!projectRules.isEmpty()) {
|
||||||
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
||||||
@@ -160,132 +186,142 @@ void ClientInterface::sendMessage(
|
|||||||
context.systemPrompt = systemPrompt;
|
context.systemPrompt = systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<LLMCore::Message> messages;
|
QVector<PluginLLMCore::Message> messages;
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::Message apiMessage;
|
PluginLLMCore::Message apiMessage;
|
||||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
||||||
apiMessage.content = msg.content;
|
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.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
||||||
apiMessage.isRedacted = msg.isRedacted;
|
apiMessage.isRedacted = msg.isRedacted;
|
||||||
apiMessage.signature = msg.signature;
|
apiMessage.signature = msg.signature;
|
||||||
|
|
||||||
if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
|
||||||
|
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
||||||
auto apiImages = loadImagesFromStorage(msg.images);
|
auto apiImages = loadImagesFromStorage(msg.images);
|
||||||
if (!apiImages.isEmpty()) {
|
if (!apiImages.isEmpty()) {
|
||||||
apiMessage.images = apiImages;
|
apiMessage.images = apiImages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.append(apiMessage);
|
messages.append(apiMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!imageFiles.isEmpty() && !provider->supportImage()) {
|
if (!imageFiles.isEmpty()
|
||||||
|
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
|
||||||
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
||||||
.arg(provider->name(), QString::number(imageFiles.size())));
|
.arg(provider->name(), QString::number(imageFiles.size())));
|
||||||
}
|
}
|
||||||
|
|
||||||
context.history = messages;
|
context.history = messages;
|
||||||
|
|
||||||
LLMCore::LLMConfig config;
|
QJsonObject payload{
|
||||||
config.requestType = LLMCore::RequestType::Chat;
|
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||||
config.provider = provider;
|
|
||||||
config.promptTemplate = promptTemplate;
|
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
|
||||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
|
||||||
config.url = QUrl(QString("%1/models/%2:%3")
|
|
||||||
.arg(
|
|
||||||
Settings::generalSettings().caUrl(),
|
|
||||||
Settings::generalSettings().caModel(),
|
|
||||||
stream));
|
|
||||||
} else {
|
|
||||||
config.url
|
|
||||||
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
|
||||||
config.providerRequest
|
|
||||||
= {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
|
||||||
}
|
|
||||||
|
|
||||||
config.apiKey = provider->apiKey();
|
provider->prepareRequest(
|
||||||
|
payload,
|
||||||
config.provider->prepareRequest(
|
|
||||||
config.providerRequest,
|
|
||||||
promptTemplate,
|
promptTemplate,
|
||||||
context,
|
context,
|
||||||
LLMCore::RequestType::Chat,
|
PluginLLMCore::RequestType::Chat,
|
||||||
isToolsEnabled,
|
useTools,
|
||||||
Settings::chatAssistantSettings().enableThinkingMode());
|
useThinking);
|
||||||
|
|
||||||
QString requestId = QUuid::createUuid().toString();
|
provider->client()->setMaxToolContinuations(
|
||||||
QJsonObject request{{"id", requestId}};
|
Settings::toolsSettings().maxToolContinuations());
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::partialResponseReceived,
|
&::LLMQore::BaseClient::chunkReceived,
|
||||||
this,
|
this,
|
||||||
&ClientInterface::handlePartialResponse,
|
&ClientInterface::handlePartialResponse,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::fullResponseReceived,
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
this,
|
this,
|
||||||
&ClientInterface::handleFullResponse,
|
&ClientInterface::handleFullResponse,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::requestFailed,
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
this,
|
this,
|
||||||
&ClientInterface::handleRequestFailed,
|
&ClientInterface::handleRequestFailed,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::toolExecutionStarted,
|
&::LLMQore::BaseClient::toolStarted,
|
||||||
m_chatModel,
|
|
||||||
&ChatModel::addToolExecutionStatus,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::toolExecutionCompleted,
|
|
||||||
m_chatModel,
|
|
||||||
&ChatModel::updateToolResult,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::continuationStarted,
|
|
||||||
this,
|
this,
|
||||||
&ClientInterface::handleCleanAccumulatedData,
|
&ClientInterface::handleToolExecutionStarted,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::thinkingBlockReceived,
|
&::LLMQore::BaseClient::toolResultReady,
|
||||||
m_chatModel,
|
this,
|
||||||
&ChatModel::addThinkingBlock,
|
&ClientInterface::handleToolExecutionCompleted,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::redactedThinkingBlockReceived,
|
&::LLMQore::BaseClient::thinkingBlockReceived,
|
||||||
m_chatModel,
|
this,
|
||||||
&ChatModel::addRedactedThinkingBlock,
|
&ClientInterface::handleThinkingBlockReceived,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
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();
|
||||||
|
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
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();
|
m_chatModel->clear();
|
||||||
LOG_MESSAGE("Chat history cleared");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
{
|
{
|
||||||
QSet<LLMCore::Provider *> providers;
|
QSet<PluginLLMCore::Provider *> providers;
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
if (it.value().provider) {
|
if (it.value().provider) {
|
||||||
providers.insert(it.value().provider);
|
providers.insert(it.value().provider);
|
||||||
@@ -293,7 +329,7 @@ void ClientInterface::cancelRequest()
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
for (auto *provider : providers) {
|
||||||
disconnect(provider, nullptr, this, nullptr);
|
disconnect(provider->client(), nullptr, this, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
@@ -305,6 +341,7 @@ void ClientInterface::cancelRequest()
|
|||||||
|
|
||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
m_accumulatedResponses.clear();
|
m_accumulatedResponses.clear();
|
||||||
|
m_awaitingContinuation.clear();
|
||||||
|
|
||||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||||
}
|
}
|
||||||
@@ -371,6 +408,12 @@ void ClientInterface::handlePartialResponse(const QString &requestId, const QStr
|
|||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
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;
|
m_accumulatedResponses[requestId] += partialText;
|
||||||
|
|
||||||
const RequestContext &ctx = it.value();
|
const RequestContext &ctx = it.value();
|
||||||
@@ -386,14 +429,14 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
|||||||
const RequestContext &ctx = it.value();
|
const RequestContext &ctx = it.value();
|
||||||
|
|
||||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
||||||
|
|
||||||
QString applyError;
|
QString applyError;
|
||||||
bool applySuccess = Context::ChangesManager::instance()
|
bool applySuccess
|
||||||
.applyPendingEditsForRequest(requestId, &applyError);
|
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
|
||||||
|
|
||||||
if (!applySuccess) {
|
if (!applySuccess) {
|
||||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||||
.arg(requestId, applyError));
|
.arg(requestId, applyError));
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
@@ -401,12 +444,9 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
|||||||
+ ": " + finalText);
|
+ ": " + finalText);
|
||||||
emit messageReceivedCompletely();
|
emit messageReceivedCompletely();
|
||||||
|
|
||||||
if (it != m_activeRequests.end()) {
|
m_activeRequests.erase(it);
|
||||||
m_activeRequests.erase(it);
|
m_accumulatedResponses.remove(requestId);
|
||||||
}
|
m_awaitingContinuation.remove(requestId);
|
||||||
if (m_accumulatedResponses.contains(requestId)) {
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||||
@@ -418,51 +458,86 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin
|
|||||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
||||||
emit errorOccurred(error);
|
emit errorOccurred(error);
|
||||||
|
|
||||||
if (it != m_activeRequests.end()) {
|
m_activeRequests.erase(it);
|
||||||
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_accumulatedResponses.contains(requestId)) {
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
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::handleCleanAccumulatedData(const QString &requestId)
|
void ClientInterface::handleToolExecutionStarted(
|
||||||
|
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||||
{
|
{
|
||||||
m_accumulatedResponses[requestId].clear();
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(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 = {
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||||
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"
|
|
||||||
};
|
|
||||||
|
|
||||||
QFileInfo fileInfo(filePath);
|
QFileInfo fileInfo(filePath);
|
||||||
QString extension = fileInfo.suffix().toLower();
|
QString extension = fileInfo.suffix().toLower();
|
||||||
|
|
||||||
return imageExtensions.contains(extension);
|
return imageExtensions.contains(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ClientInterface::getMediaTypeForImage(const QString &filePath) const
|
QString ClientInterface::getMediaTypeForImage(const QString &filePath) const
|
||||||
{
|
{
|
||||||
static const QHash<QString, QString> mediaTypes = {
|
static const QHash<QString, QString> mediaTypes
|
||||||
{"png", "image/png"},
|
= {{"png", "image/png"},
|
||||||
{"jpg", "image/jpeg"},
|
{"jpg", "image/jpeg"},
|
||||||
{"jpeg", "image/jpeg"},
|
{"jpeg", "image/jpeg"},
|
||||||
{"gif", "image/gif"},
|
{"gif", "image/gif"},
|
||||||
{"webp", "image/webp"},
|
{"webp", "image/webp"},
|
||||||
{"bmp", "image/bmp"},
|
{"bmp", "image/bmp"},
|
||||||
{"svg", "image/svg+xml"}
|
{"svg", "image/svg+xml"}};
|
||||||
};
|
|
||||||
|
|
||||||
QFileInfo fileInfo(filePath);
|
QFileInfo fileInfo(filePath);
|
||||||
QString extension = fileInfo.suffix().toLower();
|
QString extension = fileInfo.suffix().toLower();
|
||||||
|
|
||||||
if (mediaTypes.contains(extension)) {
|
if (mediaTypes.contains(extension)) {
|
||||||
return mediaTypes[extension];
|
return mediaTypes[extension];
|
||||||
}
|
}
|
||||||
|
|
||||||
QMimeDatabase mimeDb;
|
QMimeDatabase mimeDb;
|
||||||
QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
|
QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
|
||||||
return mimeType.name();
|
return mimeType.name();
|
||||||
@@ -475,37 +550,53 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
|||||||
LOG_MESSAGE(QString("Failed to open image file: %1").arg(filePath));
|
LOG_MESSAGE(QString("Failed to open image file: %1").arg(filePath));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray imageData = file.readAll();
|
QByteArray imageData = file.readAll();
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
return imageData.toBase64();
|
return imageData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const
|
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||||
|
const QList<ChatModel::ImageAttachment> &storedImages) const
|
||||||
{
|
{
|
||||||
QVector<LLMCore::ImageAttachment> apiImages;
|
QVector<PluginLLMCore::ImageAttachment> apiImages;
|
||||||
|
|
||||||
for (const auto &storedImage : storedImages) {
|
for (const auto &storedImage : storedImages) {
|
||||||
QString base64Data = ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath);
|
QString base64Data
|
||||||
|
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
||||||
if (base64Data.isEmpty()) {
|
if (base64Data.isEmpty()) {
|
||||||
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ImageAttachment apiImage;
|
PluginLLMCore::ImageAttachment apiImage;
|
||||||
apiImage.data = base64Data;
|
apiImage.data = base64Data;
|
||||||
apiImage.mediaType = storedImage.mediaType;
|
apiImage.mediaType = storedImage.mediaType;
|
||||||
apiImage.isUrl = false;
|
apiImage.isUrl = false;
|
||||||
|
|
||||||
apiImages.append(apiImage);
|
apiImages.append(apiImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiImages;
|
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,31 +1,16 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QSet>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "Provider.hpp"
|
#include "Provider.hpp"
|
||||||
#include "llmcore/IPromptProvider.hpp"
|
#include "pluginllmcore/IPromptProvider.hpp"
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@@ -36,14 +21,15 @@ class ClientInterface : public QObject
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ClientInterface(
|
explicit ClientInterface(
|
||||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||||
~ClientInterface();
|
~ClientInterface();
|
||||||
|
|
||||||
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 useAgentMode = false);
|
bool useTools = false,
|
||||||
|
bool useThinking = false);
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
@@ -61,7 +47,15 @@ private slots:
|
|||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||||
void handleCleanAccumulatedData(const QString &requestId);
|
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 handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||||
@@ -71,21 +65,22 @@ private:
|
|||||||
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<LLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
||||||
|
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
LLMCore::Provider *provider;
|
PluginLLMCore::Provider *provider;
|
||||||
};
|
};
|
||||||
|
|
||||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
QHash<QString, QString> m_accumulatedResponses;
|
QHash<QString, QString> m_accumulatedResponses;
|
||||||
|
QSet<QString> m_awaitingContinuation;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "FileItem.hpp"
|
#include "FileItem.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
426
ChatView/FileMentionItem.cpp
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "FileMentionItem.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/documentmodel.h>
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
FileMentionItem::FileMentionItem(QQuickItem *parent)
|
||||||
|
: QQuickItem(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::searchResults() const
|
||||||
|
{
|
||||||
|
return m_searchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileMentionItem::currentIndex() const
|
||||||
|
{
|
||||||
|
return m_currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::setCurrentIndex(int index)
|
||||||
|
{
|
||||||
|
if (m_currentIndex == index)
|
||||||
|
return;
|
||||||
|
m_currentIndex = index;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::updateSearch(const QString &query)
|
||||||
|
{
|
||||||
|
m_lastQuery = query;
|
||||||
|
|
||||||
|
QVariantList openFiles = getOpenFiles(query);
|
||||||
|
QVariantList projectResults = searchProjectFiles(query);
|
||||||
|
|
||||||
|
QSet<QString> openPaths;
|
||||||
|
for (const QVariant &item : std::as_const(openFiles)) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
openPaths.insert(map.value("absolutePath").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList combined = openFiles;
|
||||||
|
for (const QVariant &item : std::as_const(projectResults)) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
if (!map.value("isProject").toBool()
|
||||||
|
&& openPaths.contains(map.value("absolutePath").toString()))
|
||||||
|
continue;
|
||||||
|
combined.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_searchResults = combined;
|
||||||
|
m_currentIndex = 0;
|
||||||
|
emit searchResultsChanged();
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::refreshSearch()
|
||||||
|
{
|
||||||
|
if (!m_lastQuery.isNull())
|
||||||
|
updateSearch(m_lastQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::moveUp()
|
||||||
|
{
|
||||||
|
if (m_currentIndex > 0) {
|
||||||
|
m_currentIndex--;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::moveDown()
|
||||||
|
{
|
||||||
|
if (m_currentIndex < m_searchResults.size() - 1) {
|
||||||
|
m_currentIndex++;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::selectCurrent()
|
||||||
|
{
|
||||||
|
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
|
||||||
|
if (item.value("isProject").toBool()) {
|
||||||
|
emit projectSelected(item.value("projectName").toString());
|
||||||
|
} else {
|
||||||
|
emit fileSelected(
|
||||||
|
item.value("absolutePath").toString(),
|
||||||
|
item.value("relativePath").toString(),
|
||||||
|
item.value("projectName").toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::dismiss()
|
||||||
|
{
|
||||||
|
m_searchResults.clear();
|
||||||
|
m_currentIndex = 0;
|
||||||
|
emit searchResultsChanged();
|
||||||
|
emit currentIndexChanged();
|
||||||
|
emit dismissed();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap FileMentionItem::applyCurrentSelection(
|
||||||
|
const QString &text, int cursorPosition, bool useTools)
|
||||||
|
{
|
||||||
|
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size()) {
|
||||||
|
dismiss();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString textBefore = text.left(cursorPosition);
|
||||||
|
const int atIndex = textBefore.lastIndexOf('@');
|
||||||
|
if (atIndex < 0) {
|
||||||
|
dismiss();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
|
||||||
|
QString replacement;
|
||||||
|
|
||||||
|
if (item.value("isProject").toBool()) {
|
||||||
|
replacement = QStringLiteral("@") + item.value("projectName").toString() + ":";
|
||||||
|
} else {
|
||||||
|
const QString currentQuery = textBefore.mid(atIndex + 1);
|
||||||
|
const QVariantMap result = handleFileSelection(
|
||||||
|
item.value("absolutePath").toString(),
|
||||||
|
item.value("relativePath").toString(),
|
||||||
|
item.value("projectName").toString(),
|
||||||
|
currentQuery,
|
||||||
|
useTools);
|
||||||
|
|
||||||
|
if (result.value("mode").toString() == "mention")
|
||||||
|
replacement = result.value("mentionText").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString newText = text.left(atIndex) + replacement + text.mid(cursorPosition);
|
||||||
|
const int newCursorPosition = atIndex + replacement.length();
|
||||||
|
|
||||||
|
dismiss();
|
||||||
|
|
||||||
|
return {{"text", newText}, {"cursorPosition", newCursorPosition}};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap FileMentionItem::handleFileSelection(
|
||||||
|
const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString ¤tQuery,
|
||||||
|
bool useTools)
|
||||||
|
{
|
||||||
|
QVariantMap result;
|
||||||
|
const QString fileName = relativePath.section('/', -1);
|
||||||
|
|
||||||
|
QString mentionKey = fileName;
|
||||||
|
const int colonIdx = currentQuery.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const QString projPrefix = currentQuery.left(colonIdx);
|
||||||
|
if (projPrefix.compare(projectName, Qt::CaseInsensitive) == 0)
|
||||||
|
mentionKey = projPrefix + ":" + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useTools) {
|
||||||
|
registerMention(mentionKey, absolutePath);
|
||||||
|
result["mode"] = QStringLiteral("mention");
|
||||||
|
result["mentionText"] = "@" + mentionKey + " ";
|
||||||
|
} else {
|
||||||
|
emit fileAttachRequested({absolutePath});
|
||||||
|
result["mode"] = QStringLiteral("attach");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::registerMention(const QString &mentionKey, const QString &absolutePath)
|
||||||
|
{
|
||||||
|
m_atMentionMap[mentionKey] = absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::clearMentions()
|
||||||
|
{
|
||||||
|
m_atMentionMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::expandMentions(const QString &text)
|
||||||
|
{
|
||||||
|
QString result = text;
|
||||||
|
|
||||||
|
for (auto it = m_atMentionMap.constBegin(); it != m_atMentionMap.constEnd(); ++it) {
|
||||||
|
const QString &mentionKey = it.key();
|
||||||
|
const QString &absPath = it.value();
|
||||||
|
const QString displayName = mentionKey.section(':', -1);
|
||||||
|
const QString escaped = QRegularExpression::escape(mentionKey);
|
||||||
|
|
||||||
|
// @key:N-M -> hyperlink + inline code block
|
||||||
|
const QRegularExpression rangeRe("@" + escaped + ":(\\d+)-(\\d+)(?=\\s|$)");
|
||||||
|
QRegularExpressionMatchIterator matchIt = rangeRe.globalMatch(result);
|
||||||
|
QList<QRegularExpressionMatch> matches;
|
||||||
|
while (matchIt.hasNext())
|
||||||
|
matches.append(matchIt.next());
|
||||||
|
|
||||||
|
for (int i = matches.size() - 1; i >= 0; --i) {
|
||||||
|
const auto &m = matches[i];
|
||||||
|
const int startLine = m.captured(1).toInt();
|
||||||
|
const int endLine = m.captured(2).toInt();
|
||||||
|
const QString ext = fileExtension(absPath);
|
||||||
|
const QString snippet = readFileLines(absPath, startLine, endLine);
|
||||||
|
const QString replacement
|
||||||
|
= QString("[@%1:%2-%3](file://%4)\n```%5\n%6```")
|
||||||
|
.arg(displayName)
|
||||||
|
.arg(startLine)
|
||||||
|
.arg(endLine)
|
||||||
|
.arg(absPath, ext, snippet);
|
||||||
|
result.replace(m.capturedStart(), m.capturedLength(), replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @key -> hyperlink only
|
||||||
|
const QRegularExpression simpleRe("@" + escaped + "(?=\\s|$)");
|
||||||
|
result.replace(simpleRe, QString("[@%1](file://%2)").arg(displayName, absPath));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::searchProjectFiles(const QString &query)
|
||||||
|
{
|
||||||
|
QVariantList results;
|
||||||
|
|
||||||
|
struct FileResult
|
||||||
|
{
|
||||||
|
QString absolutePath;
|
||||||
|
QString relativePath;
|
||||||
|
QString projectName;
|
||||||
|
int priority;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto allProjects = ProjectExplorer::ProjectManager::projects();
|
||||||
|
|
||||||
|
QString projectFilter;
|
||||||
|
QString fileQuery = query;
|
||||||
|
const int colonIdx = query.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const QString prefix = query.left(colonIdx);
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (project && project->displayName().compare(prefix, Qt::CaseInsensitive) == 0) {
|
||||||
|
projectFilter = project->displayName();
|
||||||
|
fileQuery = query.mid(colonIdx + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectFilter.isEmpty() && colonIdx < 0) {
|
||||||
|
const QString lowerQ = query.toLower();
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
const QString name = project->displayName();
|
||||||
|
if (query.isEmpty() || name.toLower().startsWith(lowerQ)) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = QString();
|
||||||
|
item["relativePath"] = name;
|
||||||
|
item["projectName"] = name;
|
||||||
|
item["isProject"] = true;
|
||||||
|
results.append(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<FileResult> candidates;
|
||||||
|
const QString lowerFileQuery = fileQuery.toLower();
|
||||||
|
const bool emptyFileQuery = fileQuery.isEmpty();
|
||||||
|
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
if (!projectFilter.isEmpty() && project->displayName() != projectFilter)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||||
|
const QString projectDir = project->projectDirectory().path();
|
||||||
|
const QString projectName = project->displayName();
|
||||||
|
|
||||||
|
for (const auto &filePath : projectFiles) {
|
||||||
|
const QString absolutePath = filePath.path();
|
||||||
|
const QFileInfo fileInfo(absolutePath);
|
||||||
|
const QString fileName = fileInfo.fileName();
|
||||||
|
const QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||||
|
const QString lowerFileName = fileName.toLower();
|
||||||
|
const QString lowerRelativePath = relativePath.toLower();
|
||||||
|
|
||||||
|
int priority = -1;
|
||||||
|
if (emptyFileQuery) {
|
||||||
|
priority = 3;
|
||||||
|
} else if (lowerFileName == lowerFileQuery) {
|
||||||
|
priority = 0;
|
||||||
|
} else if (lowerFileName.startsWith(lowerFileQuery)) {
|
||||||
|
priority = 1;
|
||||||
|
} else if (lowerFileName.contains(lowerFileQuery)) {
|
||||||
|
priority = 2;
|
||||||
|
} else if (lowerRelativePath.contains(lowerFileQuery)) {
|
||||||
|
priority = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority >= 0)
|
||||||
|
candidates.append({absolutePath, relativePath, projectName, priority});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(candidates.begin(), candidates.end(), [](const FileResult &a, const FileResult &b) {
|
||||||
|
if (a.priority != b.priority)
|
||||||
|
return a.priority < b.priority;
|
||||||
|
return a.relativePath < b.relativePath;
|
||||||
|
});
|
||||||
|
|
||||||
|
const int maxFiles = qMax(0, 10 - results.size());
|
||||||
|
const int count = qMin(candidates.size(), maxFiles);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = candidates[i].absolutePath;
|
||||||
|
item["relativePath"] = candidates[i].relativePath;
|
||||||
|
item["projectName"] = candidates[i].projectName;
|
||||||
|
item["isProject"] = false;
|
||||||
|
results.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::getOpenFiles(const QString &query)
|
||||||
|
{
|
||||||
|
QVariantList results;
|
||||||
|
const QString lowerQuery = query.toLower();
|
||||||
|
const bool emptyQuery = query.isEmpty();
|
||||||
|
QSet<QString> addedPaths;
|
||||||
|
|
||||||
|
auto tryAddDocument = [&](Core::IDocument *document) {
|
||||||
|
if (!document)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString absolutePath = document->filePath().toFSPathString();
|
||||||
|
if (absolutePath.isEmpty() || addedPaths.contains(absolutePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QFileInfo fileInfo(absolutePath);
|
||||||
|
const QString fileName = fileInfo.fileName();
|
||||||
|
if (fileName.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString relativePath = absolutePath;
|
||||||
|
QString projectName;
|
||||||
|
|
||||||
|
auto project = ProjectExplorer::ProjectManager::projectForFile(document->filePath());
|
||||||
|
if (project) {
|
||||||
|
projectName = project->displayName();
|
||||||
|
relativePath = QDir(project->projectDirectory().path()).relativeFilePath(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emptyQuery) {
|
||||||
|
const QString lowerFileName = fileName.toLower();
|
||||||
|
const QString lowerRelativePath = relativePath.toLower();
|
||||||
|
if (!lowerFileName.contains(lowerQuery) && !lowerRelativePath.contains(lowerQuery))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addedPaths.insert(absolutePath);
|
||||||
|
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = absolutePath;
|
||||||
|
item["relativePath"] = relativePath;
|
||||||
|
item["projectName"] = projectName;
|
||||||
|
item["isProject"] = false;
|
||||||
|
item["isOpen"] = true;
|
||||||
|
results.append(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (auto current = Core::EditorManager::currentEditor())
|
||||||
|
tryAddDocument(current->document());
|
||||||
|
|
||||||
|
for (auto editor : Core::EditorManager::visibleEditors())
|
||||||
|
if (editor)
|
||||||
|
tryAddDocument(editor->document());
|
||||||
|
|
||||||
|
for (auto document : Core::DocumentModel::openedDocuments())
|
||||||
|
tryAddDocument(document);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::readFileLines(const QString &filePath, int startLine, int endLine)
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QTextStream stream(&file);
|
||||||
|
QString result;
|
||||||
|
int lineNum = 1;
|
||||||
|
while (!stream.atEnd()) {
|
||||||
|
const QString line = stream.readLine();
|
||||||
|
if (lineNum >= startLine)
|
||||||
|
result += line + '\n';
|
||||||
|
if (lineNum >= endLine)
|
||||||
|
break;
|
||||||
|
++lineNum;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::fileExtension(const QString &filePath)
|
||||||
|
{
|
||||||
|
const int dot = filePath.lastIndexOf('.');
|
||||||
|
return dot >= 0 ? filePath.mid(dot + 1) : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
70
ChatView/FileMentionItem.hpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class FileMentionItem : public QQuickItem
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QVariantList searchResults READ searchResults NOTIFY searchResultsChanged FINAL)
|
||||||
|
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL)
|
||||||
|
|
||||||
|
QML_ELEMENT
|
||||||
|
public:
|
||||||
|
explicit FileMentionItem(QQuickItem *parent = nullptr);
|
||||||
|
|
||||||
|
QVariantList searchResults() const;
|
||||||
|
int currentIndex() const;
|
||||||
|
void setCurrentIndex(int index);
|
||||||
|
|
||||||
|
Q_INVOKABLE void updateSearch(const QString &query);
|
||||||
|
Q_INVOKABLE void refreshSearch();
|
||||||
|
Q_INVOKABLE void moveUp();
|
||||||
|
Q_INVOKABLE void moveDown();
|
||||||
|
Q_INVOKABLE void selectCurrent();
|
||||||
|
Q_INVOKABLE void dismiss();
|
||||||
|
|
||||||
|
Q_INVOKABLE QVariantMap handleFileSelection(
|
||||||
|
const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString ¤tQuery,
|
||||||
|
bool useTools);
|
||||||
|
|
||||||
|
Q_INVOKABLE QVariantMap applyCurrentSelection(
|
||||||
|
const QString &text, int cursorPosition, bool useTools);
|
||||||
|
|
||||||
|
Q_INVOKABLE void registerMention(const QString &mentionKey, const QString &absolutePath);
|
||||||
|
Q_INVOKABLE void clearMentions();
|
||||||
|
Q_INVOKABLE QString expandMentions(const QString &text);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchResultsChanged();
|
||||||
|
void currentIndexChanged();
|
||||||
|
void fileSelected(const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName);
|
||||||
|
void projectSelected(const QString &projectName);
|
||||||
|
void dismissed();
|
||||||
|
void fileAttachRequested(const QStringList &filePaths);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVariantList searchProjectFiles(const QString &query);
|
||||||
|
QVariantList getOpenFiles(const QString &query);
|
||||||
|
QString readFileLines(const QString &filePath, int startLine, int endLine);
|
||||||
|
static QString fileExtension(const QString &filePath);
|
||||||
|
|
||||||
|
QVariantList m_searchResults;
|
||||||
|
int m_currentIndex = 0;
|
||||||
|
QString m_lastQuery;
|
||||||
|
QHash<QString, QString> m_atMentionMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
10
ChatView/icons/compress-icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Archive/compress icon: downward arrows pointing to center with horizontal lines -->
|
||||||
|
<line x1="12" y1="3" x2="12" y2="10" />
|
||||||
|
<polyline points="9 7 12 10 15 7" />
|
||||||
|
|
||||||
|
<line x1="12" y1="21" x2="12" y2="14" />
|
||||||
|
<polyline points="9 17 12 14 15 17" />
|
||||||
|
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 487 B |
5
ChatView/icons/context-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
|
||||||
|
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 233 B |
4
ChatView/icons/settings-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 962 B |
@@ -1,4 +1,4 @@
|
|||||||
<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" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
|
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black" fill-opacity="0.6"/>
|
||||||
<path d="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
<path d="M6 35L38 6" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
11
ChatView/icons/tools-icon-off.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_82_71)">
|
||||||
|
<path d="M10.7777 0.0435181C14.2316 -0.253961 17.6161 0.979215 20.0629 3.42633C23.4139 6.77767 24.3012 11.6719 22.7299 15.8433C22.9016 15.988 23.0706 16.1419 23.2377 16.3072L42.2221 34.2203C42.2288 34.2268 42.2353 34.2344 42.2426 34.2408C44.4752 36.4735 44.4752 40.1064 42.2426 42.3394C40.0096 44.5717 36.4035 44.5446 34.1713 42.3121C34.1617 42.3031 34.1528 42.2937 34.144 42.2838L16.3871 23.1519C16.2254 22.9894 16.0746 22.8196 15.933 22.6451C11.7604 24.2194 6.86327 23.3335 3.50919 19.98C1.06298 17.5327 -0.171482 14.1483 0.126373 10.6949C0.160109 10.3034 0.41818 9.96685 0.78653 9.83258C1.15602 9.69759 1.57009 9.78945 1.84805 10.067L7.53555 15.7535L13.8402 13.7574L15.8363 7.4527L10.1488 1.7652C9.87057 1.48716 9.77945 1.07345 9.91348 0.704651C10.0489 0.335072 10.3852 0.0774496 10.7777 0.0435181ZM37.3656 34.7496L37.3129 34.9302L37.1586 35.4673L36.8363 35.5679L36.6195 35.2047L36.4623 34.942L36.2357 35.148L35.725 35.6148L35.5746 35.7525L35.6791 35.9283L35.9184 36.3287L35.7104 36.6548L35.1742 36.5543L34.9408 36.5093L34.8852 36.7418L34.7572 37.275L34.7123 37.4644L34.8842 37.5543L35.3891 37.8179V38.1802L34.8842 38.4449L34.7123 38.5347L34.7572 38.7242L34.8852 39.2574L34.9408 39.4898L35.1742 39.4449L35.7104 39.3433L35.9184 39.6695L35.6791 40.0709L35.5746 40.2466L35.725 40.3843L36.2357 40.8511L36.4623 41.0572L36.6195 40.7945L36.8363 40.4302L37.1586 40.5308L37.3129 41.0689L37.3656 41.2496H38.6352L38.6879 41.0689L38.8412 40.5308L39.1635 40.4302L39.3813 40.7945L39.5385 41.0572L39.765 40.8511L40.2758 40.3843L40.4262 40.2466L40.3217 40.0709L40.0815 39.6695L40.2895 39.3433L40.8266 39.4449L41.06 39.4898L41.1156 39.2574L41.2436 38.7242L41.2885 38.5347L41.1166 38.4449L40.6117 38.1802V37.8179L41.1166 37.5543L41.2885 37.4644L41.2436 37.275L41.1156 36.7418L41.06 36.5093L40.8266 36.5543L40.2895 36.6548L40.0815 36.3287L40.3217 35.9283L40.4262 35.7525L40.2758 35.6148L39.765 35.148L39.5385 34.942L39.3813 35.2047L39.1635 35.5679L38.8412 35.4673L38.6879 34.9302L38.6352 34.7496H37.3656Z" fill="black" fill-opacity="0.6"/>
|
||||||
|
<path d="M6 36L38 7" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_82_71">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
10
ChatView/icons/tools-icon-on.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_82_50)">
|
||||||
|
<path d="M10.7775 0.0441895C14.2315 -0.253375 17.6159 0.979824 20.0627 3.427C23.4135 6.77842 24.3011 11.6726 22.7297 15.844C22.9013 15.9886 23.0714 16.1416 23.2385 16.3069L42.2219 34.2209C42.2285 34.2274 42.2352 34.2341 42.2424 34.2405C44.475 36.4732 44.475 40.1061 42.2424 42.3391C40.0094 44.5715 36.4033 44.5444 34.1711 42.3118C34.1615 42.3028 34.1525 42.2934 34.1437 42.2834L16.3869 23.1516C16.2251 22.9891 16.0745 22.8193 15.9328 22.6448C11.7602 24.2191 6.86304 23.3333 3.50897 19.9797C1.06276 17.5324 -0.171773 14.148 0.12616 10.6946C0.159908 10.3029 0.418723 9.96644 0.787292 9.83228C1.15667 9.69748 1.56997 9.78926 1.84784 10.0667L7.53534 15.7532L13.84 13.7571L15.8361 7.45239L10.1486 1.76489C9.87052 1.48684 9.78022 1.07306 9.91425 0.704346C10.0498 0.334991 10.3852 0.0781082 10.7775 0.0441895ZM37.3654 34.7502L37.3127 34.9309L37.1584 35.468L36.8361 35.5686L36.6193 35.2053L36.4621 34.9426L36.2355 35.1487L35.7248 35.6155L35.5744 35.7532L35.6789 35.929L35.9182 36.3293L35.7101 36.6555L35.174 36.5549L34.9406 36.51L34.8849 36.7424L34.757 37.2756L34.7121 37.4651L34.884 37.5549L35.3889 37.8186V38.1809L34.884 38.4456L34.7121 38.5354L34.757 38.7249L34.8849 39.2581L34.9406 39.4905L35.174 39.4456L35.7101 39.344L35.9182 39.6702L35.6789 40.0715L35.5744 40.2473L35.7248 40.385L36.2355 40.8518L36.4621 41.0579L36.6193 40.7952L36.8361 40.4309L37.1584 40.5315L37.3127 41.0696L37.3654 41.2502H38.6349L38.6877 41.0696L38.841 40.5315L39.1633 40.4309L39.381 40.7952L39.5383 41.0579L39.7648 40.8518L40.2756 40.385L40.426 40.2473L40.3215 40.0715L40.0812 39.6702L40.2892 39.344L40.8264 39.4456L41.0598 39.4905L41.1154 39.2581L41.2433 38.7249L41.2883 38.5354L41.1164 38.4456L40.6115 38.1809V37.8186L41.1164 37.5549L41.2883 37.4651L41.2433 37.2756L41.1154 36.7424L41.0598 36.51L40.8264 36.5549L40.2892 36.6555L40.0812 36.3293L40.3215 35.929L40.426 35.7532L40.2756 35.6155L39.7648 35.1487L39.5383 34.9426L39.381 35.2053L39.1633 35.5686L38.841 35.468L38.6877 34.9309L38.6349 34.7502H37.3654Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_82_50">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -26,6 +10,7 @@ import UIControls
|
|||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|
||||||
import "./chatparts"
|
import "./chatparts"
|
||||||
|
import "./controls"
|
||||||
|
|
||||||
ChatRootView {
|
ChatRootView {
|
||||||
id: root
|
id: root
|
||||||
@@ -60,6 +45,7 @@ ChatRootView {
|
|||||||
|
|
||||||
SplitDropZone {
|
SplitDropZone {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
z: 99
|
||||||
|
|
||||||
onFilesDroppedToAttach: (urlStrings) => {
|
onFilesDroppedToAttach: (urlStrings) => {
|
||||||
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
||||||
@@ -76,7 +62,22 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoABusyOverlay {
|
||||||
|
id: compressingOverlay
|
||||||
|
|
||||||
|
z: 50
|
||||||
|
|
||||||
|
anchors.fill: mainColumn
|
||||||
|
anchors.topMargin: topBar.height
|
||||||
|
anchors.bottomMargin: bottomBar.height
|
||||||
|
|
||||||
|
active: root.isCompressing
|
||||||
|
text: qsTr("Compressing chat…")
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
id: mainColumn
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
@@ -96,27 +97,26 @@ ChatRootView {
|
|||||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||||
}
|
}
|
||||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||||
rulesButton.onClicked: rulesViewer.open()
|
contextButton.onClicked: contextViewer.open()
|
||||||
activeRulesCount: root.activeRulesCount
|
|
||||||
pinButton {
|
pinButton {
|
||||||
visible: typeof _chatview !== 'undefined'
|
visible: typeof _chatview !== 'undefined'
|
||||||
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
|
||||||
}
|
}
|
||||||
agentModeSwitch {
|
toolsButton {
|
||||||
checked: root.isAgentMode
|
checked: root.useTools
|
||||||
enabled: root.toolsSupportEnabled
|
onCheckedChanged: {
|
||||||
onToggled: {
|
root.useTools = toolsButton.checked
|
||||||
root.isAgentMode = agentModeSwitch.checked
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
thinkingMode {
|
thinkingMode {
|
||||||
checked: root.isThinkingMode
|
checked: root.useThinking
|
||||||
enabled: root.isThinkingSupport
|
enabled: root.isThinkingSupport
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
root.isThinkingMode = thinkingMode.checked
|
root.useThinking = thinkingMode.checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
settingsButton.onClicked: root.openSettings()
|
||||||
configSelector {
|
configSelector {
|
||||||
model: root.availableConfigurations
|
model: root.availableConfigurations
|
||||||
displayText: root.currentConfiguration
|
displayText: root.currentConfiguration
|
||||||
@@ -130,12 +130,24 @@ ChatRootView {
|
|||||||
root.loadAvailableConfigurations()
|
root.loadAvailableConfigurations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roleSelector {
|
||||||
|
model: root.availableAgentRoles
|
||||||
|
displayText: root.currentAgentRole
|
||||||
|
onActivated: function(index) {
|
||||||
|
root.applyAgentRole(root.availableAgentRoles[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.onAboutToShow: {
|
||||||
|
root.loadAvailableAgentRoles()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: chatListView
|
id: chatListView
|
||||||
|
|
||||||
signal hideServiceComponents(int itemIndex)
|
property bool userScrolledUp: false
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
@@ -146,6 +158,18 @@ ChatRootView {
|
|||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
cacheBuffer: 2000
|
cacheBuffer: 2000
|
||||||
|
|
||||||
|
onMovingChanged: {
|
||||||
|
if (moving) {
|
||||||
|
userScrolledUp = !atYEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAtYEndChanged: {
|
||||||
|
if (atYEnd) {
|
||||||
|
userScrolledUp = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: componentLoader
|
id: componentLoader
|
||||||
|
|
||||||
@@ -166,11 +190,6 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (componentLoader.sourceComponent == chatItemComponent) {
|
|
||||||
chatListView.hideServiceComponents(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header: Item {
|
header: Item {
|
||||||
@@ -182,12 +201,53 @@ ChatRootView {
|
|||||||
id: scroll
|
id: scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: scrollToBottomButton
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
bottom: parent.bottom
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
bottomMargin: 10
|
||||||
|
}
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: palette.button
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
visible: chatListView.userScrolledUp
|
||||||
|
opacity: 0.9
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "▼"
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: palette.buttonText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
chatListView.userScrolledUp = false
|
||||||
|
root.scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on visible {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onCountChanged: {
|
onCountChanged: {
|
||||||
root.scrollToBottom()
|
if (!userScrolledUp) {
|
||||||
|
root.scrollToBottom()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentHeightChanged: {
|
onContentHeightChanged: {
|
||||||
if (atYEnd) {
|
if (!userScrolledUp && atYEnd) {
|
||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +276,10 @@ ChatRootView {
|
|||||||
messageInput.cursorPosition = model.content.length
|
messageInput.cursorPosition = model.content.length
|
||||||
root.chatModel.resetModelTo(idx)
|
root.chatModel.resetModelTo(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenFileRequested: function(filePath) {
|
||||||
|
root.openFileInEditor(filePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,19 +287,8 @@ ChatRootView {
|
|||||||
id: toolMessageComponent
|
id: toolMessageComponent
|
||||||
|
|
||||||
ToolBlock {
|
ToolBlock {
|
||||||
id: toolsItem
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
toolContent: model.content
|
toolContent: model.content
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: chatListView
|
|
||||||
function onHideServiceComponents(itemIndex) {
|
|
||||||
if (index !== itemIndex) {
|
|
||||||
toolsItem.headerOpacity = 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +321,6 @@ ChatRootView {
|
|||||||
id: thinkingMessageComponent
|
id: thinkingMessageComponent
|
||||||
|
|
||||||
ThinkingBlock {
|
ThinkingBlock {
|
||||||
id: thinking
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
thinkingContent: {
|
thinkingContent: {
|
||||||
let content = model.content
|
let content = model.content
|
||||||
@@ -280,15 +331,6 @@ ChatRootView {
|
|||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
|
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: chatListView
|
|
||||||
function onHideServiceComponents(itemIndex) {
|
|
||||||
if (index !== itemIndex) {
|
|
||||||
thinking.headerOpacity = 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,7 +368,38 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
onTextChanged: {
|
||||||
|
root.calculateMessageTokensCount(messageInput.text)
|
||||||
|
var cursorPos = messageInput.cursorPosition
|
||||||
|
var textBefore = messageInput.text.substring(0, cursorPos)
|
||||||
|
var atIndex = textBefore.lastIndexOf('@')
|
||||||
|
if (atIndex >= 0) {
|
||||||
|
var query = textBefore.substring(atIndex + 1)
|
||||||
|
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
||||||
|
fileMentionPopup.updateSearch(query)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileMentionPopup.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: function(event) {
|
||||||
|
if (fileMentionPopup.visible) {
|
||||||
|
if (event.key === Qt.Key_Down) {
|
||||||
|
fileMentionPopup.moveDown()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
fileMentionPopup.moveUp()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
root.applyMentionSelection()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
fileMentionPopup.dismiss()
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -416,12 +489,16 @@ ChatRootView {
|
|||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 40
|
||||||
|
|
||||||
|
isCompressing: root.isCompressing
|
||||||
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-icon.svg"
|
||||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||||
|
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
|
||||||
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
||||||
: qsTr("Stop")
|
: qsTr("Stop")
|
||||||
|
compressButton.onClicked: compressConfirmDialog.open()
|
||||||
|
cancelCompressButton.onClicked: root.cancelCompression()
|
||||||
syncOpenFiles {
|
syncOpenFiles {
|
||||||
checked: root.isSyncOpenFiles
|
checked: root.isSyncOpenFiles
|
||||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||||
@@ -437,15 +514,12 @@ ChatRootView {
|
|||||||
|
|
||||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||||
context: Qt.WindowShortcut
|
context: Qt.WindowShortcut
|
||||||
onActivated: {
|
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
|
||||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
onActivated: root.sendChatMessage()
|
||||||
root.sendChatMessage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.chatModel.clear()
|
root.clearMessages()
|
||||||
root.clearAttachmentFiles()
|
root.clearAttachmentFiles()
|
||||||
root.updateInputTokensCount()
|
root.updateInputTokensCount()
|
||||||
}
|
}
|
||||||
@@ -454,12 +528,38 @@ ChatRootView {
|
|||||||
Qt.callLater(chatListView.positionViewAtEnd)
|
Qt.callLater(chatListView.positionViewAtEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyMentionSelection() {
|
||||||
|
var result = fileMentionPopup.applyCurrentSelection(
|
||||||
|
messageInput.text, messageInput.cursorPosition, root.useTools)
|
||||||
|
if (result.text !== undefined) {
|
||||||
|
messageInput.text = result.text
|
||||||
|
messageInput.cursorPosition = result.cursorPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.sendMessage(messageInput.text)
|
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
|
fileMentionPopup.clearMentions()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: compressConfirmDialog
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
title: qsTr("Compress Chat")
|
||||||
|
modal: true
|
||||||
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: root.compressCurrentChat()
|
||||||
|
}
|
||||||
|
|
||||||
Toast {
|
Toast {
|
||||||
id: errorToast
|
id: errorToast
|
||||||
z: 1000
|
z: 1000
|
||||||
@@ -478,19 +578,28 @@ ChatRootView {
|
|||||||
toastTextColor: "#FFFFFF"
|
toastTextColor: "#FFFFFF"
|
||||||
}
|
}
|
||||||
|
|
||||||
RulesViewer {
|
ContextViewer {
|
||||||
id: rulesViewer
|
id: contextViewer
|
||||||
|
|
||||||
width: parent.width * 0.8
|
width: Math.min(parent.width * 0.85, 800)
|
||||||
height: parent.height * 0.8
|
height: Math.min(parent.height * 0.85, 700)
|
||||||
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
|
activeRules: root.activeRules
|
||||||
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex)
|
activeRulesCount: root.activeRulesCount
|
||||||
|
|
||||||
onRefreshRules: root.refreshRules()
|
onOpenSettings: root.openSettings()
|
||||||
|
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||||
onOpenRulesFolder: root.openRulesFolder()
|
onOpenRulesFolder: root.openRulesFolder()
|
||||||
|
onRefreshRules: root.refreshRules()
|
||||||
|
onRuleSelected: function(index) {
|
||||||
|
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -505,6 +614,26 @@ ChatRootView {
|
|||||||
infoToast.show(root.lastInfoMessage)
|
infoToast.show(root.lastInfoMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function onOpenFilesChanged() {
|
||||||
|
if (fileMentionPopup.visible)
|
||||||
|
Qt.callLater(fileMentionPopup.refreshSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMentionPopup {
|
||||||
|
id: fileMentionPopup
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
onSelectionRequested: root.applyMentionSelection()
|
||||||
|
|
||||||
|
onFileAttachRequested: function(filePaths) {
|
||||||
|
root.addFilesToAttachList(filePaths)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ChatView
|
import ChatView
|
||||||
@@ -51,6 +35,7 @@ Rectangle {
|
|||||||
property int messageIndex: -1
|
property int messageIndex: -1
|
||||||
|
|
||||||
signal resetChatToMessage(int index)
|
signal resetChatToMessage(int index)
|
||||||
|
signal openFileRequested(string filePath)
|
||||||
|
|
||||||
height: msgColumn.implicitHeight + 10
|
height: msgColumn.implicitHeight + 10
|
||||||
radius: 8
|
radius: 8
|
||||||
@@ -121,24 +106,11 @@ Rectangle {
|
|||||||
Repeater {
|
Repeater {
|
||||||
id: attachmentsModel
|
id: attachmentsModel
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: AttachmentComponent {
|
||||||
required property int index
|
required property int index
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
height: attachText.implicitHeight + 8
|
itemData: modelData
|
||||||
width: attachText.implicitWidth + 16
|
|
||||||
radius: 4
|
|
||||||
color: palette.button
|
|
||||||
border.width: 1
|
|
||||||
border.color: palette.mid
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: attachText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,9 +165,12 @@ Rectangle {
|
|||||||
onClicked: function() {
|
onClicked: function() {
|
||||||
root.resetChatToMessage(root.messageIndex)
|
root.resetChatToMessage(root.messageIndex)
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Reset chat to this message and edit")
|
QoAToolTip {
|
||||||
ToolTip.delay: 500
|
visible: stopButtonId.hovered
|
||||||
|
text: qsTr("Reset chat to this message and edit")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component TextComponent : TextBlock {
|
component TextComponent : TextBlock {
|
||||||
@@ -217,6 +192,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLinkActivated: function(link) {
|
||||||
|
if (link.startsWith("file://")) {
|
||||||
|
var filePath = link.replace(/^file:\/\//, "")
|
||||||
|
root.openFileRequested(filePath)
|
||||||
|
} else {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ChatUtils {
|
ChatUtils {
|
||||||
id: utils
|
id: utils
|
||||||
}
|
}
|
||||||
@@ -239,6 +223,56 @@ Rectangle {
|
|||||||
codeFontSize: root.codeFontSize
|
codeFontSize: root.codeFontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component AttachmentComponent : Rectangle {
|
||||||
|
required property var itemData
|
||||||
|
|
||||||
|
height: attachFileText.implicitHeight + 8
|
||||||
|
width: attachFileText.implicitWidth + 16
|
||||||
|
radius: 4
|
||||||
|
color: attachFileMouseArea.containsMouse ? Qt.lighter(palette.button, 1.1) : palette.button
|
||||||
|
border.width: 1
|
||||||
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
FileItem {
|
||||||
|
id: fileItem
|
||||||
|
filePath: itemData.filePath || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: attachFileText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (itemData.fileName || "")
|
||||||
|
color: palette.buttonText
|
||||||
|
font.pointSize: root.textFontSize - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: attachFileMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
fileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
|
fileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: attachFileMouseArea.containsMouse
|
||||||
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
component ImageComponent : Rectangle {
|
component ImageComponent : Rectangle {
|
||||||
required property var itemData
|
required property var itemData
|
||||||
|
|
||||||
@@ -248,10 +282,17 @@ Rectangle {
|
|||||||
width: Math.min(imageDisplay.implicitWidth, maxImageWidth) + 16
|
width: Math.min(imageDisplay.implicitWidth, maxImageWidth) + 16
|
||||||
height: imageDisplay.implicitHeight + fileNameText.implicitHeight + 16
|
height: imageDisplay.implicitHeight + fileNameText.implicitHeight + 16
|
||||||
radius: 4
|
radius: 4
|
||||||
color: palette.base
|
color: imageMouseArea.containsMouse ? Qt.lighter(palette.base, 1.05) : palette.base
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: palette.mid
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
FileItem {
|
||||||
|
id: imageFileItem
|
||||||
|
filePath: itemData.filePath || ""
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 8
|
anchors.margins: 8
|
||||||
@@ -259,6 +300,7 @@ Rectangle {
|
|||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: imageDisplay
|
id: imageDisplay
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.maximumWidth: parent.parent.maxImageWidth
|
Layout.maximumWidth: parent.parent.maxImageWidth
|
||||||
Layout.maximumHeight: parent.parent.maxImageHeight
|
Layout.maximumHeight: parent.parent.maxImageHeight
|
||||||
@@ -289,6 +331,7 @@ Rectangle {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: fileNameText
|
id: fileNameText
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: itemData.fileName || ""
|
text: itemData.fileName || ""
|
||||||
color: palette.text
|
color: palette.text
|
||||||
@@ -297,5 +340,28 @@ Rectangle {
|
|||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: imageMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
imageFileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
|
imageFileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: imageMouseArea.containsMouse
|
||||||
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
@@ -29,8 +13,6 @@ TextEdit {
|
|||||||
selectionColor: palette.highlight
|
selectionColor: palette.highlight
|
||||||
color: palette.text
|
color: palette.text
|
||||||
|
|
||||||
onLinkActivated: (link) => Qt.openUrlExternally(link)
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ChatView
|
import ChatView
|
||||||
|
import UIControls
|
||||||
|
|
||||||
Flow {
|
Flow {
|
||||||
id: root
|
id: root
|
||||||
@@ -78,9 +63,11 @@ Flow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.visible: containsMouse
|
QoAToolTip {
|
||||||
ToolTip.delay: 500
|
visible: mouse.containsMouse
|
||||||
ToolTip.text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
|
delay: 500
|
||||||
|
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -31,6 +15,10 @@ Rectangle {
|
|||||||
property alias attachFiles: attachFilesId
|
property alias attachFiles: attachFilesId
|
||||||
property alias attachImages: attachImagesId
|
property alias attachImages: attachImagesId
|
||||||
property alias linkFiles: linkFilesId
|
property alias linkFiles: linkFilesId
|
||||||
|
property alias compressButton: compressButtonId
|
||||||
|
property alias cancelCompressButton: cancelCompressButtonId
|
||||||
|
|
||||||
|
property bool isCompressing: false
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
@@ -49,17 +37,6 @@ Rectangle {
|
|||||||
|
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: sendButtonId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: attachFilesId
|
id: attachFilesId
|
||||||
|
|
||||||
@@ -111,5 +88,66 @@ Rectangle {
|
|||||||
Item {
|
Item {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: compressingRow
|
||||||
|
|
||||||
|
visible: root.isCompressing
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: compressBusyIndicator
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
running: root.isCompressing
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Compressing...")
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: palette.text
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: cancelCompressButtonId
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Cancel compression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: compressButtonId
|
||||||
|
|
||||||
|
visible: !root.isCompressing
|
||||||
|
text: qsTr("Compress")
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: sendButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
542
ChatView/qml/controls/ContextViewer.qml
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls.Basic as QQC
|
||||||
|
|
||||||
|
import UIControls
|
||||||
|
import ChatView
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string baseSystemPrompt
|
||||||
|
property string currentAgentRole
|
||||||
|
property string currentAgentRoleDescription
|
||||||
|
property string currentAgentRoleSystemPrompt
|
||||||
|
property var activeRules
|
||||||
|
property int activeRulesCount
|
||||||
|
property string selectedRuleContent
|
||||||
|
|
||||||
|
signal openSettings()
|
||||||
|
signal openAgentRolesSettings()
|
||||||
|
signal openRulesFolder()
|
||||||
|
signal refreshRules()
|
||||||
|
signal ruleSelected(int index)
|
||||||
|
|
||||||
|
modal: true
|
||||||
|
focus: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: palette.window
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUtils {
|
||||||
|
id: utils
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Chat Context")
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Refresh")
|
||||||
|
onClicked: root.refreshRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Close")
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: mainFlickable
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
contentHeight: sectionsColumn.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: sectionsColumn
|
||||||
|
|
||||||
|
width: mainFlickable.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
CollapsibleSection {
|
||||||
|
id: systemPromptSection
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: qsTr("Base System Prompt")
|
||||||
|
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
|
||||||
|
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
|
||||||
|
|
||||||
|
sectionContent: ColumnLayout {
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: systemPromptFlickable
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
contentHeight: systemPromptText.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: systemPromptText
|
||||||
|
|
||||||
|
width: systemPromptFlickable.width
|
||||||
|
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
enabled: root.baseSystemPrompt.length > 0
|
||||||
|
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Edit in Settings")
|
||||||
|
onClicked: {
|
||||||
|
root.openSettings()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollapsibleSection {
|
||||||
|
id: agentRoleSection
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: qsTr("Agent Role")
|
||||||
|
badge: root.currentAgentRole
|
||||||
|
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
|
||||||
|
|
||||||
|
sectionContent: ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.currentAgentRoleDescription
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.italic: true
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.currentAgentRoleDescription.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
visible: root.currentAgentRoleSystemPrompt.length > 0
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: agentPromptFlickable
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
contentHeight: agentPromptText.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: agentPromptText
|
||||||
|
|
||||||
|
width: agentPromptFlickable.width
|
||||||
|
text: root.currentAgentRoleSystemPrompt
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: palette.text
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("No role selected. Using base system prompt only.")
|
||||||
|
font.pixelSize: 11
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.currentAgentRoleSystemPrompt.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
enabled: root.currentAgentRoleSystemPrompt.length > 0
|
||||||
|
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Manage Roles")
|
||||||
|
onClicked: {
|
||||||
|
root.openAgentRolesSettings()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollapsibleSection {
|
||||||
|
id: projectRulesSection
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: qsTr("Project Rules")
|
||||||
|
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
|
||||||
|
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
|
||||||
|
|
||||||
|
sectionContent: ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SplitView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 220
|
||||||
|
orientation: Qt.Horizontal
|
||||||
|
visible: root.activeRulesCount > 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
SplitView.minimumWidth: 120
|
||||||
|
SplitView.preferredWidth: 180
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Rules (%1)").arg(rulesList.count)
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: rulesList
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
model: root.activeRules
|
||||||
|
currentIndex: 0
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: ListView.view.width
|
||||||
|
height: ruleItemContent.implicitHeight + 8
|
||||||
|
highlighted: ListView.isCurrentItem
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: {
|
||||||
|
if (parent.highlighted)
|
||||||
|
return palette.highlight
|
||||||
|
if (parent.hovered)
|
||||||
|
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
||||||
|
return "transparent"
|
||||||
|
}
|
||||||
|
radius: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
id: ruleItemContent
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.fileName
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.category
|
||||||
|
font.pixelSize: 9
|
||||||
|
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
rulesList.currentIndex = index
|
||||||
|
root.ruleSelected(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
SplitView.fillWidth: true
|
||||||
|
SplitView.minimumWidth: 200
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 5
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Content")
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
enabled: root.selectedRuleContent.length > 0
|
||||||
|
onClicked: utils.copyToClipboard(root.selectedRuleContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: ruleContentFlickable
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
contentHeight: ruleContentArea.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: ruleContentArea
|
||||||
|
|
||||||
|
width: ruleContentFlickable.width
|
||||||
|
text: root.selectedRuleContent
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
selectionColor: palette.highlight
|
||||||
|
color: palette.text
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
|
||||||
|
font.pixelSize: 11
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.activeRulesCount === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Open Rules Folder")
|
||||||
|
onClicked: root.openRulesFolder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
|
||||||
|
font.pixelSize: 9
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component CollapsibleSection: ColumnLayout {
|
||||||
|
id: sectionRoot
|
||||||
|
|
||||||
|
property string title
|
||||||
|
property string badge
|
||||||
|
property color badgeColor: palette.mid
|
||||||
|
property Component sectionContent: null
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 32
|
||||||
|
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sectionMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: sectionRoot.expanded = !sectionRoot.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: sectionRoot.expanded ? "▼" : "▶"
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: palette.text
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: sectionRoot.title
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitWidth: badgeText.implicitWidth + 12
|
||||||
|
implicitHeight: 18
|
||||||
|
color: sectionRoot.badgeColor
|
||||||
|
radius: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: badgeText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: sectionRoot.badge
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: "#FFFFFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 12
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
sourceComponent: sectionRoot.sectionContent
|
||||||
|
visible: sectionRoot.expanded
|
||||||
|
active: sectionRoot.expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
if (root.activeRulesCount > 0) {
|
||||||
|
root.ruleSelected(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
151
ChatView/qml/controls/FileMentionPopup.qml
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
FileMentionItem {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal selectionRequested()
|
||||||
|
|
||||||
|
visible: searchResults.length > 0
|
||||||
|
height: Math.min(searchResults.length * 36, 36 * 6) + 2
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
listView.positionViewAtIndex(root.currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: palette.window
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
readonly property bool isProject: modelData.isProject === true
|
||||||
|
readonly property bool isOpen: modelData.isOpen === true
|
||||||
|
readonly property string fileName: {
|
||||||
|
if (isProject)
|
||||||
|
return modelData.projectName
|
||||||
|
const parts = modelData.relativePath.split('/')
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
width: listView.width
|
||||||
|
height: 36
|
||||||
|
color: index === root.currentIndex
|
||||||
|
? palette.highlight
|
||||||
|
: (hoverArea.containsMouse
|
||||||
|
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
|
||||||
|
: "transparent")
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: 18
|
||||||
|
Layout.preferredHeight: 18
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 3
|
||||||
|
visible: delegateItem.isProject || delegateItem.isOpen
|
||||||
|
|
||||||
|
color: {
|
||||||
|
if (delegateItem.index === root.currentIndex)
|
||||||
|
return Qt.rgba(palette.highlightedText.r,
|
||||||
|
palette.highlightedText.g,
|
||||||
|
palette.highlightedText.b, 0.2)
|
||||||
|
if (delegateItem.isProject)
|
||||||
|
return Qt.rgba(palette.highlight.r,
|
||||||
|
palette.highlight.g,
|
||||||
|
palette.highlight.b, 0.3)
|
||||||
|
return Qt.rgba(0.2, 0.7, 0.4, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: delegateItem.isProject ? "P" : "O"
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: {
|
||||||
|
if (delegateItem.index === root.currentIndex)
|
||||||
|
return palette.highlightedText
|
||||||
|
if (delegateItem.isProject)
|
||||||
|
return palette.highlight
|
||||||
|
return Qt.rgba(0.1, 0.6, 0.3, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.preferredWidth: 160
|
||||||
|
text: delegateItem.fileName
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? palette.highlightedText
|
||||||
|
: (delegateItem.isProject ? palette.highlight : palette.text)
|
||||||
|
font.bold: true
|
||||||
|
font.italic: delegateItem.isProject
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: delegateItem.isProject
|
||||||
|
? "→"
|
||||||
|
: (delegateItem.modelData.projectName + " / " + delegateItem.modelData.relativePath)
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? (delegateItem.isProject
|
||||||
|
? palette.highlightedText
|
||||||
|
: Qt.rgba(palette.highlightedText.r,
|
||||||
|
palette.highlightedText.g,
|
||||||
|
palette.highlightedText.b, 0.7))
|
||||||
|
: palette.mid
|
||||||
|
font.pixelSize: delegateItem.isProject ? 12 : 11
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
horizontalAlignment: delegateItem.isProject ? Text.AlignLeft : Text.AlignRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hoverArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
root.currentIndex = delegateItem.index
|
||||||
|
root.selectionRequested()
|
||||||
|
}
|
||||||
|
onEntered: root.currentIndex = delegateItem.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import QtQuick.Controls.Basic as QQC
|
|
||||||
|
|
||||||
import UIControls
|
|
||||||
import ChatView
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var activeRules
|
|
||||||
|
|
||||||
property alias rulesCurrentIndex: rulesList.currentIndex
|
|
||||||
property alias ruleContentAreaText: ruleContentArea.text
|
|
||||||
|
|
||||||
signal refreshRules()
|
|
||||||
signal openRulesFolder()
|
|
||||||
|
|
||||||
modal: true
|
|
||||||
focus: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: palette.window
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatUtils {
|
|
||||||
id: utils
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Active Project Rules")
|
|
||||||
font.pixelSize: 16
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Open Folder")
|
|
||||||
onClicked: root.openRulesFolder()
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Refresh")
|
|
||||||
onClicked: root.refreshRules()
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Close")
|
|
||||||
onClicked: root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: palette.mid
|
|
||||||
}
|
|
||||||
|
|
||||||
SplitView {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
orientation: Qt.Horizontal
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
SplitView.minimumWidth: 200
|
|
||||||
SplitView.preferredWidth: parent.width * 0.3
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Rules Files (%1)").arg(rulesList.count)
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: rulesList
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
clip: true
|
|
||||||
model: root.activeRules
|
|
||||||
currentIndex: 0
|
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
highlighted: ListView.isCurrentItem
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
color: {
|
|
||||||
if (parent.highlighted) {
|
|
||||||
return palette.highlight
|
|
||||||
} else if (parent.hovered) {
|
|
||||||
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
|
||||||
}
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
radius: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: ColumnLayout {
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: modelData.fileName
|
|
||||||
font.pixelSize: 11
|
|
||||||
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Category: %1").arg(modelData.category)
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
rulesList.currentIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: QQC.ScrollBar {
|
|
||||||
id: scroll
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: rulesList.count === 0
|
|
||||||
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
|
|
||||||
font.pixelSize: 10
|
|
||||||
color: palette.mid
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.alignment: Qt.AlignCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
SplitView.fillWidth: true
|
|
||||||
color: palette.base
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 5
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Content")
|
|
||||||
font.pixelSize: 12
|
|
||||||
font.bold: true
|
|
||||||
color: palette.text
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
text: qsTr("Copy")
|
|
||||||
enabled: ruleContentArea.text.length > 0
|
|
||||||
onClicked: utils.copyToClipboard(ruleContentArea.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollView {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: ruleContentArea
|
|
||||||
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
selectionColor: palette.highlight
|
|
||||||
color: palette.text
|
|
||||||
font.family: "monospace"
|
|
||||||
font.pixelSize: 11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
|
|
||||||
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
|
|
||||||
font.pixelSize: 9
|
|
||||||
color: palette.mid
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -23,10 +7,12 @@ import QtQuick.Controls
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
signal filesDroppedToAttach(var urlStrings) // Array of URL strings (file://...)
|
signal filesDroppedToAttach(var urlStrings)
|
||||||
signal filesDroppedToLink(var urlStrings) // Array of URL strings (file://...)
|
signal filesDroppedToLink(var urlStrings)
|
||||||
|
|
||||||
property string activeZone: ""
|
property string activeZone: ""
|
||||||
|
property int filesCount: 0
|
||||||
|
property bool isDragActive: false
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: splitDropOverlay
|
id: splitDropOverlay
|
||||||
@@ -34,12 +20,39 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: false
|
visible: false
|
||||||
z: 999
|
z: 999
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
topMargin: 30
|
||||||
|
}
|
||||||
|
width: fileCountText.width + 40
|
||||||
|
height: 50
|
||||||
|
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
|
||||||
|
radius: 25
|
||||||
|
visible: root.filesCount > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: fileCountText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("%n file(s) to drop", "", root.filesCount)
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: palette.highlightedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: leftZone
|
id: leftZone
|
||||||
|
|
||||||
@@ -76,19 +89,20 @@ Item {
|
|||||||
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(for one-time use)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
ColorAnimation { duration: 150 }
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
}
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -127,19 +141,20 @@ Item {
|
|||||||
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(added to context)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
ColorAnimation { duration: 150 }
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
}
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -193,42 +208,67 @@ Item {
|
|||||||
|
|
||||||
onEntered: (drag) => {
|
onEntered: (drag) => {
|
||||||
if (drag.hasUrls) {
|
if (drag.hasUrls) {
|
||||||
|
root.isDragActive = true
|
||||||
|
root.filesCount = drag.urls.length
|
||||||
splitDropOverlay.visible = true
|
splitDropOverlay.visible = true
|
||||||
|
splitDropOverlay.opacity = 1
|
||||||
root.activeZone = ""
|
root.activeZone = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: {
|
onExited: {
|
||||||
splitDropOverlay.visible = false
|
root.isDragActive = false
|
||||||
root.activeZone = ""
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
|
Qt.callLater(function() {
|
||||||
|
if (!root.isDragActive) {
|
||||||
|
splitDropOverlay.visible = false
|
||||||
|
root.activeZone = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onPositionChanged: (drag) => {
|
onPositionChanged: (drag) => {
|
||||||
if (drag.x < globalDropArea.width / 2) {
|
if (drag.hasUrls) {
|
||||||
root.activeZone = "left"
|
root.activeZone = drag.x < globalDropArea.width / 2 ? "left" : "right"
|
||||||
} else {
|
|
||||||
root.activeZone = "right"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDropped: (drop) => {
|
onDropped: (drop) => {
|
||||||
var targetZone = root.activeZone
|
const targetZone = root.activeZone
|
||||||
splitDropOverlay.visible = false
|
root.isDragActive = false
|
||||||
root.activeZone = ""
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
if (drop.hasUrls && drop.urls.length > 0) {
|
Qt.callLater(function() {
|
||||||
// Convert URLs to array of strings for C++ processing
|
splitDropOverlay.visible = false
|
||||||
var urlStrings = []
|
root.activeZone = ""
|
||||||
for (var i = 0; i < drop.urls.length; i++) {
|
})
|
||||||
urlStrings.push(drop.urls[i].toString())
|
|
||||||
}
|
if (!drop.hasUrls || drop.urls.length === 0) {
|
||||||
|
return
|
||||||
if (targetZone === "right") {
|
}
|
||||||
root.filesDroppedToLink(urlStrings)
|
|
||||||
} else {
|
var urlStrings = []
|
||||||
root.filesDroppedToAttach(urlStrings)
|
for (var i = 0; i < drop.urls.length; i++) {
|
||||||
|
var urlString = drop.urls[i].toString()
|
||||||
|
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
|
||||||
|
urlStrings.push(urlString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (urlStrings.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
drop.accept(Qt.CopyAction)
|
||||||
|
|
||||||
|
if (targetZone === "right") {
|
||||||
|
root.filesDroppedToLink(urlStrings)
|
||||||
|
} else {
|
||||||
|
root.filesDroppedToAttach(urlStrings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
@@ -33,11 +17,12 @@ Rectangle {
|
|||||||
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 rulesButton: rulesButtonId
|
property alias contextButton: contextButtonId
|
||||||
property alias agentModeSwitch: agentModeSwitchId
|
property alias toolsButton: toolsButtonId
|
||||||
property alias thinkingMode: thinkingModeId
|
property alias thinkingMode: thinkingModeId
|
||||||
property alias activeRulesCount: activeRulesCountId.text
|
property alias settingsButton: settingsButtonId
|
||||||
property alias configSelector: configSelectorId
|
property alias configSelector: configSelectorId
|
||||||
|
property alias roleSelector: roleSelector
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
@@ -53,7 +38,8 @@ Rectangle {
|
|||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
height: agentModeSwitchId.height
|
id: firstRow
|
||||||
|
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -75,23 +61,61 @@ Rectangle {
|
|||||||
: qsTr("Pin chat window to the top")
|
: qsTr("Pin chat window to the top")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoATextSlider {
|
QoAComboBox {
|
||||||
id: agentModeSwitchId
|
id: configSelectorId
|
||||||
|
|
||||||
|
implicitHeight: 25
|
||||||
|
|
||||||
|
model: []
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Switch saved AI configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAComboBox {
|
||||||
|
id: roleSelector
|
||||||
|
|
||||||
|
implicitHeight: 25
|
||||||
|
|
||||||
|
model: []
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Switch agent role (different system prompts)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: toolsButtonId
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
leftText: "chat"
|
checkable: true
|
||||||
rightText: "AI Agent"
|
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.visible: hovered
|
||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: {
|
ToolTip.text: {
|
||||||
if (!agentModeSwitchId.enabled) {
|
if (!toolsButtonId.enabled) {
|
||||||
return qsTr("Tools are disabled in General Settings")
|
return qsTr("Tools are disabled in General Settings")
|
||||||
}
|
}
|
||||||
return checked
|
return checked
|
||||||
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
|
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
||||||
: qsTr("Chat Mode: Simple conversation without tool access")
|
: qsTr("Tools disabled: Simple conversation without tool access")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +141,32 @@ Rectangle {
|
|||||||
: qsTr("Thinking Mode disabled"))
|
: qsTr("Thinking Mode disabled"))
|
||||||
: qsTr("Thinking Mode is not available for this provider")
|
: qsTr("Thinking Mode is not available for this provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: settingsButtonId
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/settings-icon.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoASeparator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
height: agentModeSwitchId.height
|
height: firstRow.height
|
||||||
width: recentPathId.width
|
width: recentPathId.width
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -144,7 +190,10 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: secondRow
|
||||||
|
|
||||||
Layout.preferredWidth: root.width
|
Layout.preferredWidth: root.width
|
||||||
|
Layout.preferredHeight: firstRow.height
|
||||||
|
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
@@ -174,19 +223,6 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Load chat from *.json file")
|
ToolTip.text: qsTr("Load chat from *.json file")
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: openChatHistoryId
|
id: openChatHistoryId
|
||||||
|
|
||||||
@@ -200,36 +236,21 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Show in system")
|
ToolTip.text: qsTr("Show in system")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoASeparator {}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: rulesButtonId
|
id: contextButtonId
|
||||||
|
|
||||||
icon {
|
icon {
|
||||||
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
|
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
text: " "
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: root.activeRulesCount > 0
|
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
|
||||||
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
|
|
||||||
: qsTr("View active project rules (no rules found)")
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: activeRulesCountId
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
bottom: parent.bottom
|
|
||||||
bottomMargin: 2
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Badge {
|
Badge {
|
||||||
@@ -240,15 +261,19 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoASeparator {}
|
||||||
id: configSelectorId
|
|
||||||
|
|
||||||
model: []
|
QoAButton {
|
||||||
currentIndex: 0
|
id: clearButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: qsTr("Switch AI configuration")
|
ToolTip.text: qsTr("Clean chat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 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
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
#include "ConfigurationManager.hpp"
|
||||||
|
|
||||||
@@ -41,7 +25,7 @@ void ConfigurationManager::init()
|
|||||||
|
|
||||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
||||||
{
|
{
|
||||||
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||||
|
|
||||||
if (!templ) {
|
if (!templ) {
|
||||||
return;
|
return;
|
||||||
@@ -65,7 +49,7 @@ void ConfigurationManager::updateAllTemplateDescriptions()
|
|||||||
|
|
||||||
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
||||||
{
|
{
|
||||||
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||||
|
|
||||||
if (templ->name() == templateAspect.value())
|
if (templ->name() == templateAspect.value())
|
||||||
return;
|
return;
|
||||||
@@ -86,8 +70,8 @@ void ConfigurationManager::checkAllTemplate()
|
|||||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_generalSettings(Settings::generalSettings())
|
, m_generalSettings(Settings::generalSettings())
|
||||||
, m_providersManager(LLMCore::ProvidersManager::instance())
|
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
|
||||||
, m_templateManger(LLMCore::PromptTemplateManager::instance())
|
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ConfigurationManager::setupConnections()
|
void ConfigurationManager::setupConnections()
|
||||||
@@ -170,28 +154,26 @@ void ConfigurationManager::selectModel()
|
|||||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
||||||
: m_generalSettings.caUrl.volatileValue();
|
: m_generalSettings.caUrl.volatileValue();
|
||||||
|
|
||||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
|
||||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||||
: isQuickRefactor ? m_generalSettings.qrModel
|
: isQuickRefactor ? m_generalSettings.qrModel
|
||||||
: m_generalSettings.caModel;
|
: m_generalSettings.caModel);
|
||||||
|
|
||||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||||
if (!provider->supportsModelListing()) {
|
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
|
||||||
m_generalSettings.showModelsNotSupportedDialog(targetSettings);
|
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto modelList = provider->getInstalledModels(providerUrl);
|
provider->getInstalledModels(providerUrl)
|
||||||
|
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
||||||
if (modelList.isEmpty()) {
|
if (modelList.isEmpty()) {
|
||||||
m_generalSettings.showModelsNotFoundDialog(targetSettings);
|
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_generalSettings.showSelectionDialog(
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||||
m_generalSettings.showSelectionDialog(
|
});
|
||||||
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,12 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
#include "llmcore/PromptTemplateManager.hpp"
|
#include "pluginllmcore/PromptTemplateManager.hpp"
|
||||||
#include "llmcore/ProvidersManager.hpp"
|
#include "pluginllmcore/ProvidersManager.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
@@ -54,8 +38,8 @@ private:
|
|||||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
||||||
|
|
||||||
Settings::GeneralSettings &m_generalSettings;
|
Settings::GeneralSettings &m_generalSettings;
|
||||||
LLMCore::ProvidersManager &m_providersManager;
|
PluginLLMCore::ProvidersManager &m_providersManager;
|
||||||
LLMCore::PromptTemplateManager &m_templateManger;
|
PluginLLMCore::PromptTemplateManager &m_templateManger;
|
||||||
|
|
||||||
void setupConnections();
|
void setupConnections();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,9 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
@@ -29,16 +14,15 @@
|
|||||||
#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 <llmcore/RequestConfig.hpp>
|
#include <pluginllmcore/RulesLoader.hpp>
|
||||||
#include <llmcore/RulesLoader.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,
|
||||||
LLMCore::IProviderRegistry &providerRegistry,
|
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||||
LLMCore::IPromptProvider *promptProvider,
|
PluginLLMCore::IPromptProvider *promptProvider,
|
||||||
Context::IDocumentReader &documentReader,
|
Context::IDocumentReader &documentReader,
|
||||||
IRequestPerformanceLogger &performanceLogger)
|
IRequestPerformanceLogger &performanceLogger)
|
||||||
: m_generalSettings(generalSettings)
|
: m_generalSettings(generalSettings)
|
||||||
@@ -85,14 +69,15 @@ void LLMClientInterface::handleRequestFailed(const QString &requestId, const QSt
|
|||||||
if (it == m_activeRequests.end())
|
if (it == m_activeRequests.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
|
||||||
|
|
||||||
// Send LSP error response to client
|
|
||||||
const RequestContext &ctx = it.value();
|
const RequestContext &ctx = it.value();
|
||||||
|
|
||||||
|
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] = ctx.originalRequest["id"];
|
||||||
|
|
||||||
QJsonObject errorObject;
|
QJsonObject errorObject;
|
||||||
errorObject["code"] = -32603; // Internal error code
|
errorObject["code"] = -32603; // Internal error code
|
||||||
errorObject["message"] = error;
|
errorObject["message"] = error;
|
||||||
@@ -122,11 +107,8 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
} else if (method == "textDocument/didOpen") {
|
} else if (method == "textDocument/didOpen") {
|
||||||
handleTextDocumentDidOpen(request);
|
handleTextDocumentDidOpen(request);
|
||||||
} else if (method == "getCompletionsCycling") {
|
} else if (method == "getCompletionsCycling") {
|
||||||
QString requestId = request["id"].toString();
|
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
|
||||||
handleCompletion(request);
|
handleCompletion(request);
|
||||||
} else if (method == "cancelRequest") {
|
} else if (method == "$/cancelRequest") {
|
||||||
qDebug() << "Cancelling request";
|
|
||||||
handleCancelRequest();
|
handleCancelRequest();
|
||||||
} else if (method == "exit") {
|
} else if (method == "exit") {
|
||||||
// TODO make exit handler
|
// TODO make exit handler
|
||||||
@@ -137,7 +119,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
|
|
||||||
void LLMClientInterface::handleCancelRequest()
|
void LLMClientInterface::handleCancelRequest()
|
||||||
{
|
{
|
||||||
QSet<LLMCore::Provider *> providers;
|
QSet<PluginLLMCore::Provider *> providers;
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
if (it.value().provider) {
|
if (it.value().provider) {
|
||||||
providers.insert(it.value().provider);
|
providers.insert(it.value().provider);
|
||||||
@@ -145,7 +127,7 @@ void LLMClientInterface::handleCancelRequest()
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto *provider : providers) {
|
for (auto *provider : providers) {
|
||||||
disconnect(provider, nullptr, this, nullptr);
|
disconnect(provider->client(), nullptr, this, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
@@ -271,39 +253,24 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor to dynamic presets system
|
QJsonObject payload{{"model", modelName}, {"stream", true}};
|
||||||
LLMCore::LLMConfig config;
|
|
||||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
|
||||||
config.provider = provider;
|
|
||||||
config.promptTemplate = promptTemplate;
|
|
||||||
// TODO refactor networking
|
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
|
||||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
|
||||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
|
||||||
} else {
|
|
||||||
config.url = QUrl(
|
|
||||||
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
|
||||||
config.providerRequest = {{"model", modelName}, {"stream", true}};
|
|
||||||
}
|
|
||||||
config.apiKey = provider->apiKey();
|
|
||||||
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
|
|
||||||
|
|
||||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
|
||||||
if (!stopWords.isEmpty())
|
if (!stopWords.isEmpty())
|
||||||
config.providerRequest["stop"] = stopWords;
|
payload["stop"] = stopWords;
|
||||||
|
|
||||||
QString systemPrompt;
|
QString systemPrompt;
|
||||||
if (m_completeSettings.useSystemPrompt())
|
if (m_completeSettings.useSystemPrompt())
|
||||||
systemPrompt.append(
|
systemPrompt.append(
|
||||||
m_completeSettings.useUserMessageTemplateForCC()
|
m_completeSettings.useUserMessageTemplateForCC()
|
||||||
&& promptTemplate->type() == LLMCore::TemplateType::Chat
|
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
|
||||||
? m_completeSettings.systemPromptForNonFimModels()
|
? m_completeSettings.systemPromptForNonFimModels()
|
||||||
: m_completeSettings.systemPrompt());
|
: m_completeSettings.systemPrompt());
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||||
if (project) {
|
if (project) {
|
||||||
QString projectRules
|
QString projectRules
|
||||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions);
|
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
if (!projectRules.isEmpty()) {
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||||
@@ -315,10 +282,10 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
systemPrompt.append(updatedContext.fileContext.value());
|
systemPrompt.append(updatedContext.fileContext.value());
|
||||||
|
|
||||||
if (m_completeSettings.useOpenFilesContext()) {
|
if (m_completeSettings.useOpenFilesContext()) {
|
||||||
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
|
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
|
||||||
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
||||||
if (!updatedContext.filesMetadata) {
|
if (!updatedContext.filesMetadata) {
|
||||||
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
|
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
|
||||||
}
|
}
|
||||||
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
||||||
}
|
}
|
||||||
@@ -329,7 +296,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
|
|
||||||
updatedContext.systemPrompt = systemPrompt;
|
updatedContext.systemPrompt = systemPrompt;
|
||||||
|
|
||||||
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
|
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
|
||||||
QString userMessage;
|
QString userMessage;
|
||||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
||||||
userMessage = m_completeSettings.processMessageToFIM(
|
userMessage = m_completeSettings.processMessageToFIM(
|
||||||
@@ -339,50 +306,39 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor add message
|
// TODO refactor add message
|
||||||
QVector<LLMCore::Message> messages;
|
QVector<PluginLLMCore::Message> messages;
|
||||||
messages.append({"user", userMessage});
|
messages.append({"user", userMessage});
|
||||||
updatedContext.history = messages;
|
updatedContext.history = messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.provider->prepareRequest(
|
provider->prepareRequest(
|
||||||
config.providerRequest,
|
payload,
|
||||||
promptTemplate,
|
promptTemplate,
|
||||||
updatedContext,
|
updatedContext,
|
||||||
LLMCore::RequestType::CodeCompletion,
|
PluginLLMCore::RequestType::CodeCompletion,
|
||||||
false,
|
false,
|
||||||
false);
|
false);
|
||||||
|
|
||||||
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
QString error = QString("Request validation failed: %1").arg(errors.join("; "));
|
|
||||||
LOG_MESSAGE("Validate errors for request:");
|
|
||||||
LOG_MESSAGES(errors);
|
|
||||||
sendErrorResponse(request, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString requestId = request["id"].toString();
|
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::fullResponseReceived,
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
this,
|
this,
|
||||||
&LLMClientInterface::handleFullResponse,
|
&LLMClientInterface::handleFullResponse,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::requestFailed,
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
this,
|
this,
|
||||||
&LLMClientInterface::handleRequestFailed,
|
&LLMClientInterface::handleRequestFailed,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
auto requestId
|
||||||
|
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
||||||
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
m_performanceLogger.startTimeMeasurement(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData LLMClientInterface::prepareContext(
|
PluginLLMCore::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();
|
||||||
@@ -396,24 +352,12 @@ LLMCore::ContextData LLMClientInterface::prepareContext(
|
|||||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString LLMClientInterface::endpoint(
|
QString LLMClientInterface::resolveEndpoint(
|
||||||
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
|
||||||
{
|
{
|
||||||
QString endpoint;
|
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||||
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
|
: m_generalSettings.ccCustomEndpoint();
|
||||||
: m_generalSettings.ccEndpointMode.stringValue();
|
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
|
||||||
if (endpointMode == "Auto") {
|
|
||||||
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
|
||||||
: provider->chatEndpoint();
|
|
||||||
} else if (endpointMode == "Custom") {
|
|
||||||
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
|
||||||
: m_generalSettings.ccCustomEndpoint();
|
|
||||||
} else if (endpointMode == "FIM") {
|
|
||||||
endpoint = provider->completionEndpoint();
|
|
||||||
} else if (endpointMode == "Chat") {
|
|
||||||
endpoint = provider->chatEndpoint();
|
|
||||||
}
|
|
||||||
return endpoint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -25,9 +9,9 @@
|
|||||||
#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 <llmcore/ContextData.hpp>
|
#include <pluginllmcore/ContextData.hpp>
|
||||||
#include <llmcore/IPromptProvider.hpp>
|
#include <pluginllmcore/IPromptProvider.hpp>
|
||||||
#include <llmcore/IProviderRegistry.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>
|
||||||
@@ -45,8 +29,8 @@ public:
|
|||||||
LLMClientInterface(
|
LLMClientInterface(
|
||||||
const Settings::GeneralSettings &generalSettings,
|
const Settings::GeneralSettings &generalSettings,
|
||||||
const Settings::CodeCompletionSettings &completeSettings,
|
const Settings::CodeCompletionSettings &completeSettings,
|
||||||
LLMCore::IProviderRegistry &providerRegistry,
|
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||||
LLMCore::IPromptProvider *promptProvider,
|
PluginLLMCore::IPromptProvider *promptProvider,
|
||||||
Context::IDocumentReader &documentReader,
|
Context::IDocumentReader &documentReader,
|
||||||
IRequestPerformanceLogger &performanceLogger);
|
IRequestPerformanceLogger &performanceLogger);
|
||||||
~LLMClientInterface() override;
|
~LLMClientInterface() override;
|
||||||
@@ -82,17 +66,19 @@ private:
|
|||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
LLMCore::Provider *provider;
|
PluginLLMCore::Provider *provider;
|
||||||
};
|
};
|
||||||
|
|
||||||
LLMCore::ContextData prepareContext(
|
PluginLLMCore::ContextData prepareContext(
|
||||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||||
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
|
|
||||||
|
QString resolveEndpoint(
|
||||||
|
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;
|
||||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||||
LLMCore::IProviderRegistry &m_providerRegistry;
|
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
||||||
Context::IDocumentReader &m_documentReader;
|
Context::IDocumentReader &m_documentReader;
|
||||||
IRequestPerformanceLogger &m_performanceLogger;
|
IRequestPerformanceLogger &m_performanceLogger;
|
||||||
QElapsedTimer m_completionTimer;
|
QElapsedTimer m_completionTimer;
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
/*
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* The Qt Company portions:
|
|
||||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
|
||||||
*
|
|
||||||
* Petr Mironychev portions:
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
@@ -29,56 +9,43 @@
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
static QStringList extractTokens(const QString &str)
|
static bool isClosingTail(const QString &s, int from)
|
||||||
{
|
{
|
||||||
QStringList tokens;
|
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||||
QString currentToken;
|
for (int i = from; i < s.size(); ++i) {
|
||||||
for (const QChar &ch : str) {
|
const QChar c = s.at(i);
|
||||||
if (ch.isLetterOrNumber() || ch == '_') {
|
if (!c.isSpace() && !closeChars.contains(c))
|
||||||
currentToken += ch;
|
return false;
|
||||||
} else {
|
|
||||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
|
||||||
tokens.append(currentToken);
|
|
||||||
}
|
|
||||||
currentToken.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
return true;
|
||||||
tokens.append(currentToken);
|
|
||||||
}
|
|
||||||
return tokens;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int LLMSuggestion::calculateReplaceLength(const QString &suggestion,
|
int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText)
|
||||||
const QString &rightText,
|
|
||||||
const QString &entireLine)
|
|
||||||
{
|
{
|
||||||
if (rightText.isEmpty()) {
|
if (rightText.isEmpty())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
const int maxN = qMin(suggestion.size(), rightText.size());
|
||||||
|
int lcp = 0;
|
||||||
|
while (lcp < maxN && suggestion.at(lcp) == rightText.at(lcp))
|
||||||
|
++lcp;
|
||||||
|
|
||||||
|
if (lcp > 0) {
|
||||||
|
if (isClosingTail(rightText, lcp))
|
||||||
|
return rightText.size();
|
||||||
|
return lcp;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString structuralChars = "{}[]()<>;,";
|
if (!isClosingTail(rightText, 0))
|
||||||
bool hasStructuralOverlap = false;
|
return 0;
|
||||||
for (const QChar &ch : structuralChars) {
|
|
||||||
if (suggestion.contains(ch) && rightText.contains(ch)) {
|
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||||
hasStructuralOverlap = true;
|
int i = suggestion.size() - 1;
|
||||||
break;
|
while (i >= 0 && suggestion.at(i).isSpace())
|
||||||
}
|
--i;
|
||||||
}
|
if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i)))
|
||||||
|
return rightText.size();
|
||||||
if (hasStructuralOverlap) {
|
|
||||||
return rightText.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList suggestionTokens = extractTokens(suggestion);
|
|
||||||
const QStringList lineTokens = extractTokens(entireLine);
|
|
||||||
|
|
||||||
for (const auto &token : suggestionTokens) {
|
|
||||||
if (lineTokens.contains(token)) {
|
|
||||||
return rightText.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,22 +69,21 @@ LLMSuggestion::LLMSuggestion(
|
|||||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||||
|
|
||||||
QString suggestionText = data.text;
|
QString suggestionText = data.text;
|
||||||
QString entireLine = blockText;
|
|
||||||
|
|
||||||
if (!suggestionText.contains('\n')) {
|
if (!suggestionText.contains('\n')) {
|
||||||
int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine);
|
int replaceLength = calculateReplaceLength(suggestionText, rightText);
|
||||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
||||||
|
|
||||||
QString displayText = leftText + suggestionText + remainingRightText;
|
QString displayText = leftText + suggestionText + remainingRightText;
|
||||||
replacementDocument()->setPlainText(displayText);
|
replacementDocument()->setPlainText(displayText);
|
||||||
} else {
|
} else {
|
||||||
int firstLineEnd = suggestionText.indexOf('\n');
|
int firstLineEnd = suggestionText.indexOf('\n');
|
||||||
QString firstLine = suggestionText.left(firstLineEnd);
|
QString firstLine = suggestionText.left(firstLineEnd);
|
||||||
QString restOfCompletion = suggestionText.mid(firstLineEnd);
|
QString restOfCompletion = suggestionText.mid(firstLineEnd);
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(firstLine, rightText, entireLine);
|
int replaceLength = calculateReplaceLength(firstLine, rightText);
|
||||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
||||||
|
|
||||||
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
|
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
|
||||||
replacementDocument()->setPlainText(displayText);
|
replacementDocument()->setPlainText(displayText);
|
||||||
}
|
}
|
||||||
@@ -167,10 +133,9 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
|||||||
if (startPos == 0) {
|
if (startPos == 0) {
|
||||||
QTextBlock currentBlock = cursor.block();
|
QTextBlock currentBlock = cursor.block();
|
||||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||||
QString entireLine = currentBlock.text();
|
|
||||||
|
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
if (replaceLength > 0) {
|
||||||
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
currentCursor.removeSelectedText();
|
currentCursor.removeSelectedText();
|
||||||
@@ -220,9 +185,7 @@ bool LLMSuggestion::apply()
|
|||||||
QString text = currentData.text;
|
QString text = currentData.text;
|
||||||
|
|
||||||
QTextBlock currentBlock = cursor.block();
|
QTextBlock currentBlock = cursor.block();
|
||||||
QString textBeforeCursor = currentBlock.text().left(cursor.positionInBlock());
|
|
||||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||||
QString entireLine = currentBlock.text();
|
|
||||||
|
|
||||||
QTextCursor editCursor = cursor;
|
QTextCursor editCursor = cursor;
|
||||||
editCursor.beginEditBlock();
|
editCursor.beginEditBlock();
|
||||||
@@ -232,22 +195,22 @@ bool LLMSuggestion::apply()
|
|||||||
QString firstLine = text.left(firstLineEnd);
|
QString firstLine = text.left(firstLineEnd);
|
||||||
QString restOfText = text.mid(firstLineEnd);
|
QString restOfText = text.mid(firstLineEnd);
|
||||||
|
|
||||||
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine);
|
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor);
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
if (replaceLength > 0) {
|
||||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
editCursor.removeSelectedText();
|
editCursor.removeSelectedText();
|
||||||
}
|
}
|
||||||
|
|
||||||
editCursor.insertText(firstLine + restOfText);
|
editCursor.insertText(firstLine + restOfText);
|
||||||
} else {
|
} else {
|
||||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||||
|
|
||||||
if (replaceLength > 0) {
|
if (replaceLength > 0) {
|
||||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
editCursor.removeSelectedText();
|
editCursor.removeSelectedText();
|
||||||
}
|
}
|
||||||
|
|
||||||
editCursor.insertText(text);
|
editCursor.insertText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -42,8 +42,6 @@ public:
|
|||||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||||
bool apply() override;
|
bool apply() override;
|
||||||
|
|
||||||
static int calculateReplaceLength(const QString &suggestion,
|
static int calculateReplaceLength(const QString &suggestion, const QString &rightText);
|
||||||
const QString &rightText,
|
|
||||||
const QString &entireLine);
|
|
||||||
};
|
};
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.9.1",
|
"Version" : "0.9.12",
|
||||||
"CompatVersion" : "${IDE_VERSION}",
|
"CompatVersion" : "${IDE_VERSION}",
|
||||||
"Vendor" : "Petr Mironychev",
|
"Vendor" : "Petr Mironychev",
|
||||||
"VendorId" : "petrmironychev",
|
"VendorId" : "petrmironychev",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -40,6 +40,9 @@
|
|||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProjectSettings.hpp"
|
#include "settings/ProjectSettings.hpp"
|
||||||
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
|
#include "RefactorContextHelper.hpp"
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
|
|
||||||
@@ -51,6 +54,90 @@ using namespace Core;
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
|
||||||
|
{
|
||||||
|
return Utils::Text::Position{pos.line, pos.column};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIdentifierChar(QChar c)
|
||||||
|
{
|
||||||
|
return c.isLetterOrNumber() || c == QLatin1Char('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInsideIdentifier(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
if (col <= 0 || col > text.size())
|
||||||
|
return false;
|
||||||
|
if (!isIdentifierChar(text.at(col - 1)))
|
||||||
|
return false;
|
||||||
|
return col < text.size() && isIdentifierChar(text.at(col));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAfterMemberAccess(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
if (col <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int i = col - 1;
|
||||||
|
while (i >= 0 && isIdentifierChar(text.at(i)))
|
||||||
|
--i;
|
||||||
|
|
||||||
|
if (i < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const QChar c = text.at(i);
|
||||||
|
if (c == QLatin1Char('.'))
|
||||||
|
return true;
|
||||||
|
if (c == QLatin1Char('>') && i >= 1 && text.at(i - 1) == QLatin1Char('-'))
|
||||||
|
return true;
|
||||||
|
if (c == QLatin1Char(':') && i >= 1 && text.at(i - 1) == QLatin1Char(':'))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isFreshIndentedLine(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
if (col == 0)
|
||||||
|
return false;
|
||||||
|
const QString leftText = block.text().left(col);
|
||||||
|
for (const QChar &ch : leftText) {
|
||||||
|
if (!ch.isSpace())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAfterEagerTrigger(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
int i = col - 1;
|
||||||
|
while (i >= 0 && text.at(i).isSpace())
|
||||||
|
--i;
|
||||||
|
if (i < 0)
|
||||||
|
return false;
|
||||||
|
const QChar c = text.at(i);
|
||||||
|
return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(',')
|
||||||
|
|| c == QLatin1Char('=') || c == QLatin1Char('[') || c == QLatin1Char(';')
|
||||||
|
|| c == QLatin1Char(':') || c == QLatin1Char('>');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isManualMode()
|
||||||
|
{
|
||||||
|
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
|
||||||
|
}
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||||
: LanguageClient::Client(clientInterface)
|
: LanguageClient::Client(clientInterface)
|
||||||
, m_llmClient(clientInterface)
|
, m_llmClient(clientInterface)
|
||||||
@@ -66,17 +153,15 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
|
|
||||||
m_typingTimer.start();
|
m_typingTimer.start();
|
||||||
|
|
||||||
m_hintHideTimer.setSingleShot(true);
|
|
||||||
m_hintHideTimer.setInterval(Settings::codeCompletionSettings().hintHideTimeout());
|
|
||||||
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
|
|
||||||
|
|
||||||
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
||||||
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
delete m_refactorHoverHandler;
|
delete m_refactorHoverHandler;
|
||||||
|
delete m_refactorWidgetHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||||
@@ -103,6 +188,9 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (isManualMode())
|
||||||
|
return;
|
||||||
|
|
||||||
auto project = ProjectManager::projectForFile(document->filePath());
|
auto project = ProjectManager::projectForFile(document->filePath());
|
||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
@@ -126,38 +214,29 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
if (charsRemoved > 0 || charsAdded <= 0) {
|
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||||
m_recentCharCount = 0;
|
m_recentCharCount = 0;
|
||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
// 0 = Hint-based, 1 = Automatic
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode != 1) {
|
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextCursor cursor = widget->textCursor();
|
QTextCursor cursor = widget->textCursor();
|
||||||
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||||
QString lastChar = cursor.selectedText();
|
const QString lastChar = cursor.selectedText();
|
||||||
|
if (lastChar.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
|
const QChar lastCh = lastChar[0];
|
||||||
|
if (lastCh == QLatin1Char('\n') || lastCh == QChar::ParagraphSeparator
|
||||||
|
|| lastCh == QChar::LineSeparator) {
|
||||||
m_recentCharCount = 0;
|
m_recentCharCount = 0;
|
||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
// 0 = Hint-based, 1 = Automatic
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode != 1) {
|
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSpaceOrTab = lastChar[0].isSpace();
|
const bool isSpaceOrTab = lastCh.isSpace();
|
||||||
bool ignoreWhitespace
|
const bool ignoreWhitespace
|
||||||
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
|
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
|
||||||
|
|
||||||
if (!ignoreWhitespace || !isSpaceOrTab) {
|
if (!ignoreWhitespace || !isSpaceOrTab)
|
||||||
m_recentCharCount += charsAdded;
|
m_recentCharCount += charsAdded;
|
||||||
}
|
|
||||||
|
|
||||||
if (m_typingTimer.elapsed()
|
if (m_typingTimer.elapsed()
|
||||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
||||||
@@ -165,13 +244,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0 = Hint-based, 1 = Automatic
|
handleAutoRequestTrigger(widget);
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode == 1) {
|
|
||||||
handleAutoRequestTrigger(widget, charsAdded, isSpaceOrTab);
|
|
||||||
} else {
|
|
||||||
handleHintBasedTrigger(widget, charsAdded, isSpaceOrTab, cursor);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,11 +273,9 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
|||||||
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||||
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 0) {
|
|
||||||
editor->abortAssist();
|
editor->abortAssist();
|
||||||
}
|
|
||||||
|
|
||||||
const FilePath filePath = editor->textDocument()->filePath();
|
const FilePath filePath = editor->textDocument()->filePath();
|
||||||
GetCompletionRequest request{
|
GetCompletionRequest request{
|
||||||
@@ -265,33 +336,29 @@ void QodeAssistClient::requestQuickRefactor(
|
|||||||
|
|
||||||
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
cancelRunningRequest(editor);
|
if (m_runningRequests.contains(editor)) {
|
||||||
|
if (Settings::codeCompletionSettings().cancelOnInput())
|
||||||
|
cancelRunningRequest(editor);
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto it = m_scheduledRequests.find(editor);
|
auto it = m_scheduledRequests.find(editor);
|
||||||
if (it == m_scheduledRequests.end()) {
|
if (it == m_scheduledRequests.end()) {
|
||||||
auto timer = new QTimer(this);
|
auto timer = new QTimer(this);
|
||||||
timer->setSingleShot(true);
|
timer->setSingleShot(true);
|
||||||
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
||||||
if (editor
|
if (!editor || m_runningRequests.contains(editor))
|
||||||
&& editor->textCursor().position()
|
return;
|
||||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
|
if (editor->textCursor().position()
|
||||||
&& m_recentCharCount
|
!= m_scheduledRequests[editor]->property("cursorPosition").toInt())
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
|
return;
|
||||||
requestCompletions(editor);
|
requestCompletions(editor);
|
||||||
});
|
});
|
||||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||||
delete m_scheduledRequests.take(editor);
|
delete m_scheduledRequests.take(editor);
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(editor);
|
||||||
});
|
});
|
||||||
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
|
|
||||||
cancelRunningRequest(editor);
|
|
||||||
// 0 = Hint-based, 1 = Automatic
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode != 1) {
|
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
it = m_scheduledRequests.insert(editor, timer);
|
it = m_scheduledRequests.insert(editor, timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,11 +369,9 @@ void QodeAssistClient::handleCompletions(
|
|||||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||||
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 1) {
|
|
||||||
editor->abortAssist();
|
editor->abortAssist();
|
||||||
}
|
|
||||||
|
|
||||||
if (response.error()) {
|
if (response.error()) {
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
@@ -320,12 +385,25 @@ void QodeAssistClient::handleCompletions(
|
|||||||
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
||||||
|
|
||||||
const MultiTextCursor cursors = editor->multiTextCursor();
|
const MultiTextCursor cursors = editor->multiTextCursor();
|
||||||
if (cursors.hasMultipleCursors())
|
if (cursors.hasMultipleCursors() || cursors.hasSelection())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition)
|
const int currentPosition = cursors.mainCursor().position();
|
||||||
|
if (requestPosition < 0 || currentPosition < requestPosition)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
QString typedSinceRequest;
|
||||||
|
if (currentPosition > requestPosition) {
|
||||||
|
QTextCursor diffCursor(editor->document());
|
||||||
|
diffCursor.setPosition(requestPosition);
|
||||||
|
diffCursor.setPosition(currentPosition, QTextCursor::KeepAnchor);
|
||||||
|
typedSinceRequest = diffCursor.selectedText();
|
||||||
|
if (typedSinceRequest.contains(QChar::ParagraphSeparator)
|
||||||
|
|| typedSinceRequest.contains(QLatin1Char('\n'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
||||||
auto isValidCompletion = [](const Completion &completion) {
|
auto isValidCompletion = [](const Completion &completion) {
|
||||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||||
@@ -333,34 +411,58 @@ void QodeAssistClient::handleCompletions(
|
|||||||
QList<Completion> completions
|
QList<Completion> completions
|
||||||
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
||||||
|
|
||||||
|
QList<Completion> matchedCompletions;
|
||||||
|
matchedCompletions.reserve(completions.size());
|
||||||
for (Completion &completion : completions) {
|
for (Completion &completion : completions) {
|
||||||
const LanguageServerProtocol::Range range = completion.range();
|
const LanguageServerProtocol::Range range = completion.range();
|
||||||
if (range.start().line() != range.end().line())
|
if (range.start().line() != range.end().line())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const QString completionText = completion.text();
|
QString completionText = completion.text();
|
||||||
const int end = int(completionText.size()) - 1;
|
const int end = int(completionText.size()) - 1;
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
while (delta <= end && completionText[end - delta].isSpace())
|
while (delta <= end && completionText[end - delta].isSpace())
|
||||||
++delta;
|
++delta;
|
||||||
|
|
||||||
if (delta > 0)
|
if (delta > 0)
|
||||||
completion.setText(completionText.chopped(delta));
|
completionText.chop(delta);
|
||||||
|
|
||||||
|
if (!typedSinceRequest.isEmpty()) {
|
||||||
|
if (!completionText.startsWith(typedSinceRequest))
|
||||||
|
continue;
|
||||||
|
completionText = completionText.mid(typedSinceRequest.size());
|
||||||
|
if (completionText.isEmpty())
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
completion.setText(completionText);
|
||||||
|
matchedCompletions.append(completion);
|
||||||
}
|
}
|
||||||
auto suggestions = Utils::transform(completions, [](const Completion &c) {
|
|
||||||
|
if (matchedCompletions.isEmpty()) {
|
||||||
|
LOG_MESSAGE("No valid completions received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Text::Position anchor = typedSinceRequest.isEmpty()
|
||||||
|
? Text::Position{}
|
||||||
|
: Text::Position::fromPositionInDocument(editor->document(), currentPosition);
|
||||||
|
const bool useAnchor = !typedSinceRequest.isEmpty();
|
||||||
|
|
||||||
|
auto suggestions = Utils::transform(matchedCompletions,
|
||||||
|
[useAnchor, &anchor](const Completion &c) {
|
||||||
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
||||||
return Text::Position{pos.line() + 1, pos.character()};
|
return Text::Position{pos.line() + 1, pos.character()};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (useAnchor) {
|
||||||
|
return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
|
||||||
|
}
|
||||||
|
|
||||||
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
||||||
Text::Position pos{toTextPos(c.position())};
|
Text::Position pos{toTextPos(c.position())};
|
||||||
return TextSuggestion::Data{range, pos, c.text()};
|
return TextSuggestion::Data{range, pos, c.text()};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (completions.isEmpty()) {
|
|
||||||
LOG_MESSAGE("No valid completions received");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,12 +473,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
|||||||
if (it == m_runningRequests.constEnd())
|
if (it == m_runningRequests.constEnd())
|
||||||
return;
|
return;
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
// 0 = Hint-based, 1 = Automatic
|
|
||||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
|
||||||
if (triggerMode != 1) {
|
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
|
||||||
cancelRequest(it->id());
|
cancelRequest(it->id());
|
||||||
m_runningRequests.erase(it);
|
m_runningRequests.erase(it);
|
||||||
}
|
}
|
||||||
@@ -418,17 +514,6 @@ void QodeAssistClient::cleanupConnections()
|
|||||||
m_scheduledRequests.clear();
|
m_scheduledRequests.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QodeAssistClient::isHintVisible() const
|
|
||||||
{
|
|
||||||
return m_hintHandler.isHintVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QodeAssistClient::hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor)
|
|
||||||
{
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
requestCompletions(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||||
{
|
{
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
@@ -451,11 +536,18 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditorWidget *editorWidget = result.editor;
|
int displayMode = Settings::quickRefactorSettings().displayMode();
|
||||||
|
|
||||||
|
if (displayMode == 0) {
|
||||||
|
displayRefactoringWidget(result);
|
||||||
|
} else {
|
||||||
|
displayRefactoringSuggestion(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto toTextPos = [](const Utils::Text::Position &pos) {
|
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
|
||||||
return Utils::Text::Position{pos.line, pos.column};
|
{
|
||||||
};
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
|
|
||||||
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
||||||
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
|
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
|
||||||
@@ -510,58 +602,95 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
|
||||||
int charsAdded,
|
|
||||||
bool isSpaceOrTab)
|
|
||||||
{
|
{
|
||||||
Q_UNUSED(isSpaceOrTab);
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
|
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
||||||
if (m_recentCharCount
|
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
|
||||||
scheduleRequest(widget);
|
|
||||||
|
QString displayOriginal;
|
||||||
|
QString displayRefactored;
|
||||||
|
QString textToApply = result.newText;
|
||||||
|
|
||||||
|
if (ctx.isInsertion) {
|
||||||
|
bool isMultiline = result.newText.contains('\n');
|
||||||
|
|
||||||
|
if (isMultiline) {
|
||||||
|
displayOriginal = ctx.textBeforeCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText;
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
|
||||||
|
textToApply = result.newText;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.originalText;
|
||||||
|
displayRefactored = result.newText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
|
||||||
|
applyRefactoringEdit(editorWidget, result.insertRange, editedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setDeclineCallback([]() {});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->showRefactorWidget(
|
||||||
|
editorWidget, displayOriginal, displayRefactored, range,
|
||||||
|
ctx.contextBefore, ctx.contextAfter);
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setTextToApply(textToApply);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::handleHintBasedTrigger(TextEditor::TextEditorWidget *widget,
|
void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
|
||||||
int charsAdded,
|
const Utils::Text::Range &range,
|
||||||
bool isSpaceOrTab,
|
const QString &text)
|
||||||
QTextCursor &cursor)
|
|
||||||
{
|
{
|
||||||
Q_UNUSED(charsAdded);
|
const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
|
||||||
|
const QTextCursor endCursor = range.end.toTextCursor(editor->document());
|
||||||
|
const int startPos = startCursor.position();
|
||||||
|
const int endPos = endCursor.position();
|
||||||
|
|
||||||
|
QTextCursor editCursor(editor->document());
|
||||||
|
editCursor.beginEditBlock();
|
||||||
|
|
||||||
const int hintThreshold = Settings::codeCompletionSettings().hintCharThreshold();
|
if (startPos == endPos) {
|
||||||
if (m_recentCharCount >= hintThreshold && !isSpaceOrTab) {
|
bool isMultiline = text.contains('\n');
|
||||||
const QRect cursorRect = widget->cursorRect(cursor);
|
editCursor.setPosition(startPos);
|
||||||
QPoint globalPos = widget->viewport()->mapToGlobal(cursorRect.topLeft());
|
|
||||||
QPoint localPos = widget->mapFromGlobal(globalPos);
|
if (isMultiline) {
|
||||||
|
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
int fontSize = widget->font().pixelSize();
|
editCursor.removeSelectedText();
|
||||||
if (fontSize <= 0) {
|
|
||||||
fontSize = widget->fontMetrics().height();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextCursor textCursor = widget->textCursor();
|
editCursor.insertText(text);
|
||||||
|
} else {
|
||||||
if (m_recentCharCount <= hintThreshold) {
|
editCursor.setPosition(startPos);
|
||||||
textCursor
|
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||||
.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, m_recentCharCount);
|
editCursor.removeSelectedText();
|
||||||
} else {
|
editCursor.insertText(text);
|
||||||
textCursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, hintThreshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
int x = localPos.x() + cursorRect.height();
|
|
||||||
int y = localPos.y() + cursorRect.height() / 4;
|
|
||||||
|
|
||||||
QPoint hintPos(x, y);
|
|
||||||
|
|
||||||
if (!m_hintHandler.isHintVisible()) {
|
|
||||||
m_hintHandler.showHint(widget, hintPos, fontSize);
|
|
||||||
} else {
|
|
||||||
m_hintHandler.updateHintPosition(widget, hintPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_hintHideTimer.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editCursor.endEditBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
|
||||||
|
{
|
||||||
|
const QTextCursor cursor = widget->textCursor();
|
||||||
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
const bool smart = settings.smartContextTrigger();
|
||||||
|
|
||||||
|
if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
|
||||||
|
const int charThreshold = settings.autoCompletionCharThreshold();
|
||||||
|
|
||||||
|
if (eager || m_recentCharCount > charThreshold)
|
||||||
|
scheduleRequest(widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||||
@@ -573,46 +702,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
|||||||
if (event->type() == QEvent::KeyPress) {
|
if (event->type() == QEvent::KeyPress) {
|
||||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
|
||||||
// Check hint trigger key (0=Space, 1=Ctrl+Space, 2=Alt+Space, 3=Ctrl+Enter, 4=Tab, 5=Enter)
|
|
||||||
if (m_hintHandler.isHintVisible()) {
|
|
||||||
const int triggerKeyIndex = Settings::codeCompletionSettings().hintTriggerKey();
|
|
||||||
bool isMatchingKey = false;
|
|
||||||
const Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
|
|
||||||
|
|
||||||
switch (triggerKeyIndex) {
|
|
||||||
case 0: // Space
|
|
||||||
isMatchingKey = (keyEvent->key() == Qt::Key_Space
|
|
||||||
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
|
|
||||||
break;
|
|
||||||
case 1: // Ctrl+Space
|
|
||||||
isMatchingKey = (keyEvent->key() == Qt::Key_Space
|
|
||||||
&& (modifiers & Qt::ControlModifier));
|
|
||||||
break;
|
|
||||||
case 2: // Alt+Space
|
|
||||||
isMatchingKey = (keyEvent->key() == Qt::Key_Space
|
|
||||||
&& (modifiers & Qt::AltModifier));
|
|
||||||
break;
|
|
||||||
case 3: // Ctrl+Enter
|
|
||||||
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
|
|
||||||
&& (modifiers & Qt::ControlModifier));
|
|
||||||
break;
|
|
||||||
case 4: // Tab
|
|
||||||
isMatchingKey = (keyEvent->key() == Qt::Key_Tab);
|
|
||||||
break;
|
|
||||||
case 5: // Enter
|
|
||||||
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
|
|
||||||
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMatchingKey) {
|
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
requestCompletions(editor);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyEvent->key() == Qt::Key_Escape) {
|
if (keyEvent->key() == Qt::Key_Escape) {
|
||||||
if (m_runningRequests.contains(editor)) {
|
if (m_runningRequests.contains(editor)) {
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(editor);
|
||||||
@@ -630,8 +719,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_progressHandler.hideProgress();
|
m_progressHandler.hideProgress();
|
||||||
m_hintHideTimer.stop();
|
|
||||||
m_hintHandler.hideHint();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
/*
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* The Qt Company portions:
|
|
||||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
|
||||||
*
|
|
||||||
* Petr Mironychev portions:
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -32,11 +12,11 @@
|
|||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
#include "RefactorSuggestionHoverHandler.hpp"
|
||||||
#include "widgets/CompletionProgressHandler.hpp"
|
#include "widgets/CompletionProgressHandler.hpp"
|
||||||
#include "widgets/CompletionErrorHandler.hpp"
|
#include "widgets/CompletionErrorHandler.hpp"
|
||||||
#include "widgets/CompletionHintHandler.hpp"
|
|
||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
#include <llmcore/IPromptProvider.hpp>
|
#include <pluginllmcore/IPromptProvider.hpp>
|
||||||
#include <llmcore/IProviderRegistry.hpp>
|
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
@@ -53,9 +33,6 @@ public:
|
|||||||
void requestCompletions(TextEditor::TextEditorWidget *editor);
|
void requestCompletions(TextEditor::TextEditorWidget *editor);
|
||||||
void requestQuickRefactor(
|
void requestQuickRefactor(
|
||||||
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
||||||
|
|
||||||
bool isHintVisible() const;
|
|
||||||
void hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
@@ -70,9 +47,11 @@ private:
|
|||||||
void setupConnections();
|
void setupConnections();
|
||||||
void cleanupConnections();
|
void cleanupConnections();
|
||||||
void handleRefactoringResult(const RefactorResult &result);
|
void handleRefactoringResult(const RefactorResult &result);
|
||||||
|
void displayRefactoringSuggestion(const RefactorResult &result);
|
||||||
|
void displayRefactoringWidget(const RefactorResult &result);
|
||||||
|
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
||||||
|
|
||||||
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
|
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
|
||||||
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
|
|
||||||
|
|
||||||
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
||||||
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
||||||
@@ -81,13 +60,12 @@ private:
|
|||||||
|
|
||||||
QElapsedTimer m_typingTimer;
|
QElapsedTimer m_typingTimer;
|
||||||
int m_recentCharCount;
|
int m_recentCharCount;
|
||||||
QTimer m_hintHideTimer;
|
|
||||||
CompletionProgressHandler m_progressHandler;
|
CompletionProgressHandler m_progressHandler;
|
||||||
CompletionErrorHandler m_errorHandler;
|
CompletionErrorHandler m_errorHandler;
|
||||||
CompletionHintHandler m_hintHandler;
|
|
||||||
EditorChatButtonHandler m_chatButtonHandler;
|
EditorChatButtonHandler m_chatButtonHandler;
|
||||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||||
|
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||||
LLMClientInterface *m_llmClient;
|
LLMClientInterface *m_llmClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,25 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "QuickRefactorHandler.hpp"
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#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 <llmcore/PromptTemplateManager.hpp>
|
#include <pluginllmcore/PromptTemplateManager.hpp>
|
||||||
#include <llmcore/ProvidersManager.hpp>
|
#include <pluginllmcore/ProvidersManager.hpp>
|
||||||
#include <llmcore/RequestConfig.hpp>
|
#include <pluginllmcore/RulesLoader.hpp>
|
||||||
#include <llmcore/RulesLoader.hpp>
|
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
#include <settings/ChatAssistantSettings.hpp>
|
#include <settings/ChatAssistantSettings.hpp>
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
#include <settings/QuickRefactorSettings.hpp>
|
#include <settings/QuickRefactorSettings.hpp>
|
||||||
|
#include <settings/ToolsSettings.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
@@ -108,8 +94,8 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
|||||||
{
|
{
|
||||||
auto &settings = Settings::generalSettings();
|
auto &settings = Settings::generalSettings();
|
||||||
|
|
||||||
auto &providerRegistry = LLMCore::ProvidersManager::instance();
|
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
|
||||||
auto &promptManager = LLMCore::PromptTemplateManager::instance();
|
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
|
||||||
|
|
||||||
const auto providerName = settings.qrProvider();
|
const auto providerName = settings.qrProvider();
|
||||||
auto provider = providerRegistry.getProviderByName(providerName);
|
auto provider = providerRegistry.getProviderByName(providerName);
|
||||||
@@ -139,70 +125,57 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::LLMConfig config;
|
QJsonObject payload{
|
||||||
config.requestType = LLMCore::RequestType::QuickRefactoring;
|
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
||||||
config.provider = provider;
|
|
||||||
config.promptTemplate = promptTemplate;
|
|
||||||
config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint());
|
|
||||||
config.apiKey = provider->apiKey();
|
|
||||||
|
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
|
||||||
config.url = QUrl(QString("%1/models/%2:%3")
|
|
||||||
.arg(
|
|
||||||
Settings::generalSettings().qrUrl(),
|
|
||||||
Settings::generalSettings().qrModel(),
|
|
||||||
stream));
|
|
||||||
} else {
|
|
||||||
config.url
|
|
||||||
= QString("%1%2").arg(Settings::generalSettings().qrUrl(), provider->chatEndpoint());
|
|
||||||
config.providerRequest
|
|
||||||
= {{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
|
||||||
}
|
|
||||||
|
|
||||||
LLMCore::ContextData context = prepareContext(editor, range, instructions);
|
|
||||||
|
|
||||||
bool enableTools = Settings::quickRefactorSettings().useTools();
|
bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
||||||
provider->prepareRequest(
|
provider->prepareRequest(
|
||||||
config.providerRequest,
|
payload,
|
||||||
promptTemplate,
|
promptTemplate,
|
||||||
context,
|
context,
|
||||||
LLMCore::RequestType::QuickRefactoring,
|
PluginLLMCore::RequestType::QuickRefactoring,
|
||||||
enableTools,
|
enableTools,
|
||||||
enableThinking);
|
enableThinking);
|
||||||
|
|
||||||
QString requestId = QUuid::createUuid().toString();
|
provider->client()->setMaxToolContinuations(
|
||||||
m_lastRequestId = requestId;
|
Settings::toolsSettings().maxToolContinuations());
|
||||||
QJsonObject request{{"id", requestId}};
|
|
||||||
|
|
||||||
m_isRefactoringInProgress = true;
|
m_isRefactoringInProgress = true;
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::fullResponseReceived,
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
this,
|
this,
|
||||||
&QuickRefactorHandler::handleFullResponse,
|
&QuickRefactorHandler::handleFullResponse,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider->client(),
|
||||||
&LLMCore::Provider::requestFailed,
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
this,
|
this,
|
||||||
&QuickRefactorHandler::handleRequestFailed,
|
&QuickRefactorHandler::handleRequestFailed,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
||||||
|
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||||
|
: promptTemplate->endpoint();
|
||||||
|
auto requestId
|
||||||
|
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
||||||
|
m_lastRequestId = requestId;
|
||||||
|
QJsonObject request{{"id", requestId}};
|
||||||
|
|
||||||
|
m_activeRequests[requestId] = {request, provider};
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor,
|
||||||
const Utils::Text::Range &range,
|
const Utils::Text::Range &range,
|
||||||
const QString &instructions)
|
const QString &instructions)
|
||||||
{
|
{
|
||||||
LLMCore::ContextData context;
|
PluginLLMCore::ContextData context;
|
||||||
|
|
||||||
auto textDocument = editor->textDocument();
|
auto textDocument = editor->textDocument();
|
||||||
Context::DocumentReaderQtCreator documentReader;
|
Context::DocumentReaderQtCreator documentReader;
|
||||||
@@ -286,10 +259,10 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
|
|
||||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||||
if (project) {
|
if (project) {
|
||||||
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
|
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
||||||
project, LLMCore::RulesContext::QuickRefactor);
|
project, PluginLLMCore::RulesContext::QuickRefactor);
|
||||||
|
|
||||||
if (!projectRules.isEmpty()) {
|
if (!projectRules.isEmpty()) {
|
||||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||||
@@ -301,25 +274,73 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||||
|
|
||||||
systemPrompt += "\n\nCode context with position markers:";
|
systemPrompt += "\n\n# Code Context with Position Markers\n" + taggedContent;
|
||||||
systemPrompt += taggedContent;
|
|
||||||
|
|
||||||
systemPrompt += "\n\nOutput format:";
|
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
|
||||||
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
|
systemPrompt += cursor.hasSelection()
|
||||||
"between<selection_start><selection_end> or be "
|
? "\n- Generate ONLY the code that should REPLACE the selected text between "
|
||||||
"inserted at cursor position<cursor>";
|
"<selection_start> and <selection_end> markers"
|
||||||
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
|
"\n- Your output will completely replace the selected code"
|
||||||
"code block markers";
|
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||||
systemPrompt += "\n- The output should be ready to insert directly into the editor";
|
"\n- Your output will be inserted at the cursor location";
|
||||||
systemPrompt += "\n- Follow the existing code style and indentation patterns";
|
|
||||||
|
systemPrompt += "\n\n## Formatting Rules:"
|
||||||
|
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
||||||
|
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
||||||
|
"\n- Do NOT add comments explaining what you changed"
|
||||||
|
"\n- Do NOT repeat existing code, be precise with context"
|
||||||
|
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
||||||
|
"\n- The output must be ready to insert directly into the editor as-is";
|
||||||
|
|
||||||
|
systemPrompt += "\n\n## Indentation and Whitespace:";
|
||||||
|
|
||||||
|
if (cursor.hasSelection()) {
|
||||||
|
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : startBlock.text()) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
||||||
|
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
||||||
|
"\n- Each line in your output must maintain this base indentation")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
systemPrompt += "\n- PRESERVE all indentation from the original code";
|
||||||
|
} else {
|
||||||
|
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
||||||
|
QString lineText = block.text();
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : lineText) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation"
|
||||||
|
"\n- If generating multiline code, EVERY line must start with at least %1 spaces"
|
||||||
|
"\n- If generating single-line code, it will be inserted inline (no indentation needed)")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
||||||
|
"\n- Maintain consistent indentation for nested blocks"
|
||||||
|
"\n- Do NOT remove or reduce the base indentation level"
|
||||||
|
"\n\n## Code Style:"
|
||||||
|
"\n- Match the coding style of the surrounding code (naming, spacing, braces, etc.)"
|
||||||
|
"\n- Preserve the original code structure when possible"
|
||||||
|
"\n- Only change what is necessary to fulfill the user's request";
|
||||||
|
|
||||||
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
|
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
|
||||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||||
}
|
}
|
||||||
|
|
||||||
context.systemPrompt = systemPrompt;
|
context.systemPrompt = systemPrompt;
|
||||||
|
|
||||||
QVector<LLMCore::Message> messages;
|
QVector<PluginLLMCore::Message> messages;
|
||||||
messages.append(
|
messages.append(
|
||||||
{"user",
|
{"user",
|
||||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
||||||
@@ -338,19 +359,7 @@ void QuickRefactorHandler::handleLLMResponse(
|
|||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
|
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
|
||||||
QString cleanedResponse = response.trimmed();
|
|
||||||
if (cleanedResponse.startsWith("```")) {
|
|
||||||
int firstNewLine = cleanedResponse.indexOf('\n');
|
|
||||||
int lastFence = cleanedResponse.lastIndexOf("```");
|
|
||||||
|
|
||||||
if (firstNewLine != -1 && lastFence > firstNewLine) {
|
|
||||||
cleanedResponse
|
|
||||||
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
|
|
||||||
} else if (lastFence != -1) {
|
|
||||||
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.newText = cleanedResponse;
|
result.newText = cleanedResponse;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -27,8 +11,8 @@
|
|||||||
|
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
#include <context/IDocumentReader.hpp>
|
#include <context/IDocumentReader.hpp>
|
||||||
#include <llmcore/ContextData.hpp>
|
#include <pluginllmcore/ContextData.hpp>
|
||||||
#include <llmcore/Provider.hpp>
|
#include <pluginllmcore/Provider.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
@@ -68,7 +52,7 @@ private:
|
|||||||
const Utils::Text::Range &range);
|
const Utils::Text::Range &range);
|
||||||
|
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||||
LLMCore::ContextData prepareContext(
|
PluginLLMCore::ContextData prepareContext(
|
||||||
TextEditor::TextEditorWidget *editor,
|
TextEditor::TextEditorWidget *editor,
|
||||||
const Utils::Text::Range &range,
|
const Utils::Text::Range &range,
|
||||||
const QString &instructions);
|
const QString &instructions);
|
||||||
@@ -76,7 +60,7 @@ private:
|
|||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
LLMCore::Provider *provider;
|
PluginLLMCore::Provider *provider;
|
||||||
};
|
};
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
|
|||||||
172
README.md
@@ -1,10 +1,12 @@
|
|||||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
# QodeAssist — AI coding assistant for Qt Creator
|
||||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
|
||||||

|
|
||||||

|
|
||||||
[](https://discord.gg/BGMkUsXUgf)
|
|
||||||
|
|
||||||
 QodeAssist is a comprehensive AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion, interactive chat with multiple interface options, inline quick refactoring, and AI function calling capabilities for C++ and QML development. Supporting both local providers (Ollama, llama.cpp, LM Studio) and cloud services (Claude, OpenAI, Google AI, Mistral AI), QodeAssist enhances your productivity with context-aware AI assistance, project-specific rules, and extensive customization options directly in your Qt development environment.
|
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||||
|
[](https://github.com/Palm1r/QodeAssist/releases)
|
||||||
|

|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[](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).
|
||||||
|
|
||||||
⚠️ **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:
|
||||||
@@ -29,13 +31,15 @@
|
|||||||
|
|
||||||
QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
||||||
|
|
||||||
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML
|
- **Code Completion** — intelligent, context-aware suggestions (FIM and chat models) for C++ and QML, with multiline support
|
||||||
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel)
|
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
|
||||||
- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library
|
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
|
||||||
- **File Context**: Attach or link files for better AI understanding
|
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
|
||||||
- **Tool Calling**: AI can read project files, search code, and access diagnostics
|
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
|
||||||
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more
|
- **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)
|
||||||
- **Customizable**: Project-specific rules, custom instructions, and extensive model templates
|
- **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
|
||||||
|
- **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!
|
||||||
|
|
||||||
@@ -125,21 +129,64 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
|
### Quick Setup (Recommended for Beginners)
|
||||||
|
|
||||||
### Supported Providers
|
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
|
||||||
|
<details>
|
||||||
|
<summary>Quick setup: (click to expand)</summary>
|
||||||
|
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
|
||||||
|
</details>
|
||||||
|
|
||||||
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
|
1. **Open QodeAssist Settings**
|
||||||
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
2. **Select a Preset** - Choose from the Quick Setup dropdown:
|
||||||
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
|
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
|
||||||
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider
|
- **OpenAI** (gpt-5.2-codex)
|
||||||
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
|
- **Mistral AI** (Codestral 2501)
|
||||||
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
|
- **Google AI** (Gemini 2.5 Flash)
|
||||||
- **LM Studio** - Local LLM provider
|
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
||||||
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
|
|
||||||
|
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
||||||
|
|
||||||
|
### Manual Provider Configuration
|
||||||
|
|
||||||
|
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
|
||||||
|
|
||||||
|
**Local providers:**
|
||||||
|
- **[Ollama](docs/ollama-configuration.md)** — native Ollama API
|
||||||
|
- **Ollama (OpenAI-compatible)** — Ollama's `/v1` endpoint for tool-calling models
|
||||||
|
- **[llama.cpp](docs/llamacpp-configuration.md)** — local `llama-server`
|
||||||
|
- **LM Studio** — OpenAI-compatible Chat API
|
||||||
|
- **LM Studio (Responses API)** — newer models that require the Responses endpoint
|
||||||
|
|
||||||
|
**Cloud providers:**
|
||||||
|
- **[Anthropic Claude](docs/claude-configuration.md)** — manual setup guide
|
||||||
|
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
|
||||||
|
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
|
||||||
|
- **[Google AI](docs/google-ai-configuration.md)** — Gemini
|
||||||
|
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
||||||
|
|
||||||
|
### Recommended Models for Best Experience
|
||||||
|
|
||||||
|
For optimal coding assistance, we recommend using these top-tier models:
|
||||||
|
|
||||||
|
**Best for Code Completion & Refactoring:**
|
||||||
|
- **Claude 4.5 Haiku or Sonnet** (Anthropic)
|
||||||
|
- **GPT-5.1-codex or codex-mini** (OpenAI Responses API)
|
||||||
|
- **Codestral** (Mistral)
|
||||||
|
|
||||||
|
**Best for Chat Assistant:**
|
||||||
|
- **Claude 4.5 Sonnet** (Anthropic) - Outstanding reasoning and natural conversation flow
|
||||||
|
- **GPT-5.1-codex** (OpenAI Responses API) - Latest model with advanced capabilities
|
||||||
|
- **Gemini 2.5 or 3.0** (Google AI) - Latest models from Google
|
||||||
|
- **Mistral large** (Mistral) - Fast and capable
|
||||||
|
|
||||||
|
**Local models:**
|
||||||
|
- **Qwen3-coder** (Qwen) - Best local models
|
||||||
|
|
||||||
### Additional Configuration
|
### Additional Configuration
|
||||||
|
|
||||||
|
- **[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
|
||||||
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
- **[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`
|
||||||
|
|
||||||
@@ -155,14 +202,14 @@ QodeAssist supports multiple LLM providers. Choose your preferred provider and f
|
|||||||
|
|
||||||
QodeAssist offers two trigger modes for code completion:
|
QodeAssist offers two trigger modes for code completion:
|
||||||
|
|
||||||
**Hint-based (Default, Recommended)**
|
**Hint-based**
|
||||||
- Shows a hint indicator near cursor when you type 3+ characters
|
- Shows a hint indicator near cursor when you type 3+ characters
|
||||||
- Press **Space** (or custom key) to request completion
|
- Press **Space** (or custom key) to request completion
|
||||||
- **Best for**: Paid APIs (Claude, OpenAI), conscious control
|
- **Best for**: Paid APIs (Claude, OpenAI), conscious control
|
||||||
- **Benefits**: No unexpected API charges, full control over requests, no workflow interruption
|
- **Benefits**: No unexpected API charges, full control over requests, no workflow interruption
|
||||||
- **Visual**: Clear indicator shows when completion is ready
|
- **Visual**: Clear indicator shows when completion is ready
|
||||||
|
|
||||||
**Automatic**
|
**Automatic(Default)**
|
||||||
- Automatically requests completion after typing threshold
|
- Automatically requests completion after typing threshold
|
||||||
- Works immediately without additional keypresses
|
- Works immediately without additional keypresses
|
||||||
- **Best for**: Local models (Ollama, llama.cpp), maximum automation
|
- **Best for**: Local models (Ollama, llama.cpp), maximum automation
|
||||||
@@ -176,24 +223,51 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
|||||||
- Multiple chat panels: side panel, bottom panel, and popup window
|
- Multiple chat panels: side panel, bottom panel, and popup window
|
||||||
- Chat history with auto-save and restore
|
- Chat history with auto-save and restore
|
||||||
- Token usage monitoring
|
- Token usage monitoring
|
||||||
|
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
|
||||||
|
- **[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 mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
||||||
|
|
||||||
### Quick Refactoring
|
### Quick Refactoring
|
||||||
- Inline code refactoring directly in the editor with AI assistance
|
- Inline code refactoring directly in the editor with AI assistance
|
||||||
- Selection-based improvements with instant code replacement
|
|
||||||
- Built-in quick actions (repeat, improve, alternative)
|
|
||||||
- **Custom instructions library** with search and autocomplete
|
- **Custom instructions library** with search and autocomplete
|
||||||
- Create, edit, and manage reusable refactoring templates
|
- Create, edit, and manage reusable refactoring templates
|
||||||
- Combine base instructions with specific details
|
- Combine base instructions with specific details
|
||||||
- **[Learn more](docs/quick-refactoring.md)**
|
- **[Learn more](docs/quick-refactoring.md)**
|
||||||
|
|
||||||
### Tools & Function Calling
|
### Tools & Function Calling
|
||||||
- Read project files
|
|
||||||
- List and search in project
|
Chat and Quick Refactor can call tools to inspect and modify your project. Each tool can be individually enabled/disabled in settings.
|
||||||
- Access linter/compiler issues
|
|
||||||
- Enabled by default (can be disabled)
|
| Tool | What it does |
|
||||||
|
|------|--------------|
|
||||||
|
| `list_project_files` | List files in the active project(s) |
|
||||||
|
| `find_file` | Find a file by name or partial path |
|
||||||
|
| `read_file` | Read file contents (project or absolute path) |
|
||||||
|
| `search_project` | Grep / symbol search across project sources |
|
||||||
|
| `create_new_file` | Create a new empty file on disk |
|
||||||
|
| `edit_file` | Replace content in a file (old → new) |
|
||||||
|
| `build_project` | Build the active project and return compiler output |
|
||||||
|
| `get_issues_list` | Read current linter / compiler diagnostics |
|
||||||
|
| `execute_terminal_command` | Run a shell command (with confirmation) |
|
||||||
|
| `todo_tool` | Track multi-step task progress during a conversation |
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
- **Enable** in `Tools → Options → QodeAssist → MCP Server`
|
||||||
|
- **Transport**: HTTP + SSE by default; a stdio bridge is provided for clients that only speak stdio (e.g. Claude Desktop)
|
||||||
|
- **Ready-to-copy snippets** for Claude Code, VS Code, and the bridge are available via the "Show connection instructions" button in settings
|
||||||
|
|
||||||
|
### MCP Client Hub
|
||||||
|
|
||||||
|
QodeAssist can also act as an **MCP client**, connecting to external MCP servers and making their tools available to Chat and Quick Refactor alongside the built-in ones.
|
||||||
|
|
||||||
|
- **Configure** servers in `Tools → Options → QodeAssist → MCP Client`
|
||||||
|
- **Transports**: stdio and HTTP/SSE
|
||||||
|
- **Limitation**: authenticated MCP servers (OAuth / token-protected) are **not supported yet** — only servers that accept unauthenticated local connections work for now
|
||||||
|
|
||||||
## Context Layers
|
## Context Layers
|
||||||
|
|
||||||
@@ -262,22 +336,24 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
│ CHAT ASSISTANT │
|
│ CHAT ASSISTANT │
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ 1. System Prompt (from Chat Assistant Settings) │
|
│ 1. System Prompt (from Chat Assistant Settings) │
|
||||||
│ 2. Project Rules: │
|
│ 2. Agent Role (optional, from role selector): │
|
||||||
|
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
|
||||||
|
│ 3. Project Rules: │
|
||||||
│ ├─ .qodeassist/rules/common/*.md │
|
│ ├─ .qodeassist/rules/common/*.md │
|
||||||
│ └─ .qodeassist/rules/chat/*.md │
|
│ └─ .qodeassist/rules/chat/*.md │
|
||||||
│ 3. File Context (optional): │
|
│ 4. File Context (optional): │
|
||||||
│ ├─ Attached files (manual) │
|
│ ├─ Attached files (manual) │
|
||||||
│ ├─ Linked files (persistent) │
|
│ ├─ Linked files (persistent) │
|
||||||
│ └─ Open editor files (if auto-sync enabled) │
|
│ └─ Open editor files (if auto-sync enabled) │
|
||||||
│ 4. Tool Definitions (if enabled): │
|
│ 5. Tool Definitions (if enabled): │
|
||||||
│ ├─ ReadProjectFileByName │
|
│ ├─ ReadProjectFileByName │
|
||||||
│ ├─ ListProjectFiles │
|
│ ├─ ListProjectFiles │
|
||||||
│ ├─ SearchInProject │
|
│ ├─ SearchInProject │
|
||||||
│ └─ GetIssuesList │
|
│ └─ GetIssuesList │
|
||||||
│ 5. Conversation History │
|
│ 6. Conversation History │
|
||||||
│ 6. User Message │
|
│ 7. User Message │
|
||||||
│ │
|
│ │
|
||||||
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │
|
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
|
||||||
│ [History: Previous messages] │
|
│ [History: Previous messages] │
|
||||||
│ [User: FileContext + UserMessage] │
|
│ [User: FileContext + UserMessage] │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -323,6 +399,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
|
|
||||||
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
||||||
- **System Prompts** are configured independently for each feature in Settings
|
- **System Prompts** are configured independently for each feature in Settings
|
||||||
|
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
|
||||||
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
||||||
- FIM models: Direct completion prompt
|
- FIM models: Direct completion prompt
|
||||||
- Non-FIM models: Prompt includes response formatting instructions
|
- Non-FIM models: Prompt includes response formatting instructions
|
||||||
@@ -330,14 +407,14 @@ 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) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
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.
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
| Qt Creator Version | QodeAssist Version |
|
| Qt Creator Version | QodeAssist Version |
|
||||||
|-------------------|-------------------|
|
|-------------------|-------------------|
|
||||||
| 17.0.0+ | 0.6.0 - 0.x.x |
|
| 17.0.0+ | 0.6.0 - 0.x.x |
|
||||||
| 16.0.2 | 0.5.13 - 0.x.x |
|
| 16.0.2 | 0.5.13 - 0.9.6 |
|
||||||
| 16.0.1 | 0.5.7 - 0.5.13 |
|
| 16.0.1 | 0.5.7 - 0.5.13 |
|
||||||
| 16.0.0 | 0.5.2 - 0.5.6 |
|
| 16.0.0 | 0.5.2 - 0.5.6 |
|
||||||
| 15.0.1 | 0.4.8 - 0.5.1 |
|
| 15.0.1 | 0.4.8 - 0.5.1 |
|
||||||
@@ -372,14 +449,16 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
|
|||||||
|
|
||||||
## Development Progress
|
## Development Progress
|
||||||
|
|
||||||
- [x] Code completion functionality
|
- [x] Code completion (FIM and chat models)
|
||||||
- [x] Chat assistant with multiple panels
|
- [x] Chat assistant (side / bottom / detached panels)
|
||||||
|
- [x] Quick refactoring with custom-instructions library
|
||||||
- [x] Diff sharing with models
|
- [x] Diff sharing with models
|
||||||
- [x] Tools/function calling support
|
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
||||||
- [x] Project-specific rules
|
- [x] Project-specific rules (`.qodeassist/rules/`)
|
||||||
|
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
||||||
|
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
||||||
- [ ] Full project source sharing
|
- [ ] Full project source sharing
|
||||||
- [ ] Additional provider support
|
- [ ] Additional provider support
|
||||||
- [ ] MCP (Model Context Protocol) support
|
|
||||||
|
|
||||||
## Support the development of QodeAssist
|
## Support the development of QodeAssist
|
||||||
If you find QodeAssist helpful, there are several ways you can support the project:
|
If you find QodeAssist helpful, there are several ways you can support the project:
|
||||||
@@ -398,6 +477,11 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
|||||||
|
|
||||||
Every contribution, no matter how small, is greatly appreciated and helps keep the project alive!
|
Every contribution, no matter how small, is greatly appreciated and helps keep the project alive!
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- **[LLMQore](https://github.com/Palm1r/llmqore)** — the standalone LLM-core library extracted from QodeAssist, reusable in other Qt/C++ projects
|
||||||
|
- **[QodeAssistUpdater](https://github.com/Palm1r/QodeAssistUpdater)** — CLI installer/updater for the plugin
|
||||||
|
|
||||||
## How to Build
|
## How to Build
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|||||||
99
RefactorContextHelper.hpp
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
struct RefactorContext
|
||||||
|
{
|
||||||
|
QString originalText;
|
||||||
|
QString textBeforeCursor;
|
||||||
|
QString textAfterCursor;
|
||||||
|
QString contextBefore;
|
||||||
|
QString contextAfter;
|
||||||
|
int startPos{0};
|
||||||
|
int endPos{0};
|
||||||
|
bool isInsertion{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class RefactorContextHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static RefactorContext extractContext(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
int contextLinesBefore = 3,
|
||||||
|
int contextLinesAfter = 3)
|
||||||
|
{
|
||||||
|
RefactorContext ctx;
|
||||||
|
if (!editor) {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocument *doc = editor->document();
|
||||||
|
ctx.startPos = range.begin.toPositionInDocument(doc);
|
||||||
|
ctx.endPos = range.end.toPositionInDocument(doc);
|
||||||
|
ctx.isInsertion = (ctx.startPos == ctx.endPos);
|
||||||
|
|
||||||
|
if (!ctx.isInsertion) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.setPosition(ctx.endPos, QTextCursor::KeepAnchor);
|
||||||
|
ctx.originalText = cursor.selectedText();
|
||||||
|
ctx.originalText.replace(QChar(0x2029), "\n");
|
||||||
|
} else {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
|
||||||
|
int posInBlock = cursor.positionInBlock();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, posInBlock);
|
||||||
|
ctx.textBeforeCursor = cursor.selectedText();
|
||||||
|
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
ctx.textAfterCursor = cursor.selectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.contextBefore = extractContextLines(doc, ctx.startPos, contextLinesBefore, true);
|
||||||
|
ctx.contextAfter = extractContextLines(doc, ctx.endPos, contextLinesAfter, false);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString extractContextLines(QTextDocument *doc, int position, int lineCount, bool before)
|
||||||
|
{
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
QTextBlock currentBlock = cursor.block();
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
QTextBlock block = currentBlock.previous();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.prepend(block.text());
|
||||||
|
block = block.previous();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QTextBlock block = currentBlock.next();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.append(block.text());
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "RefactorSuggestion.hpp"
|
#include "RefactorSuggestion.hpp"
|
||||||
#include "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
#include "RefactorSuggestionHoverHandler.hpp"
|
||||||
#include "RefactorSuggestion.hpp"
|
#include "RefactorSuggestion.hpp"
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https:
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "FlowEditor.hpp"
|
#include "FlowEditor.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https:
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "GridBackground.hpp"
|
#include "GridBackground.hpp"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "BaseTask.hpp"
|
#include "BaseTask.hpp"
|
||||||
#include "TaskPort.hpp"
|
#include "TaskPort.hpp"
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "Flow.hpp"
|
#include "Flow.hpp"
|
||||||
#include "TaskPort.hpp"
|
#include "TaskPort.hpp"
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "FlowManager.hpp"
|
#include "FlowManager.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "TaskConnection.hpp"
|
#include "TaskConnection.hpp"
|
||||||
#include "BaseTask.hpp"
|
#include "BaseTask.hpp"
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "TaskPort.hpp"
|
#include "TaskPort.hpp"
|
||||||
#include "TaskConnection.hpp"
|
#include "TaskConnection.hpp"
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "TaskRegistry.hpp"
|
#include "TaskRegistry.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ qt_add_qml_module(QodeAssistUIControls
|
|||||||
QML_FILES
|
QML_FILES
|
||||||
qml/Badge.qml
|
qml/Badge.qml
|
||||||
qml/QoAButton.qml
|
qml/QoAButton.qml
|
||||||
|
qml/QoABusyOverlay.qml
|
||||||
qml/QoATextSlider.qml
|
qml/QoATextSlider.qml
|
||||||
qml/QoAComboBox.qml
|
qml/QoAComboBox.qml
|
||||||
qml/FadeListItemAnimation.qml
|
qml/FadeListItemAnimation.qml
|
||||||
|
qml/QoASeparator.qml
|
||||||
|
qml/QoAToolTip.qml
|
||||||
|
|
||||||
RESOURCES
|
RESOURCES
|
||||||
icons/dropdown-arrow-light.svg
|
icons/dropdown-arrow-light.svg
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
|
|||||||
43
UIControls/qml/QoABusyOverlay.qml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias text: label.text
|
||||||
|
property bool active: false
|
||||||
|
|
||||||
|
visible: active
|
||||||
|
color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.75)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.AllButtons
|
||||||
|
hoverEnabled: true
|
||||||
|
preventStealing: true
|
||||||
|
onWheel: function(wheel) { wheel.accepted = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
running: root.active
|
||||||
|
implicitWidth: 36
|
||||||
|
implicitHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: label
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: palette.text
|
||||||
|
font.pixelSize: 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls.Basic
|
import QtQuick.Controls.Basic
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -24,13 +8,32 @@ import QtQuick.Controls.Basic as Basic
|
|||||||
Basic.ComboBox {
|
Basic.ComboBox {
|
||||||
id: control
|
id: control
|
||||||
|
|
||||||
|
property real popupContentWidth: 100
|
||||||
|
|
||||||
implicitWidth: Math.min(contentItem.implicitWidth + 8, 300)
|
implicitWidth: Math.min(contentItem.implicitWidth + 8, 300)
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
|
|
||||||
|
function updatePopupWidth() {
|
||||||
|
var maxWidth = 100;
|
||||||
|
if (model) {
|
||||||
|
for (var i = 0; i < model.length; i++) {
|
||||||
|
textMetrics.text = model[i];
|
||||||
|
maxWidth = Math.max(maxWidth, textMetrics.width + 40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupContentWidth = Math.min(maxWidth, 350);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModelChanged: updatePopupWidth()
|
||||||
|
Component.onCompleted: updatePopupWidth()
|
||||||
|
clip: true
|
||||||
|
|
||||||
indicator: Image {
|
indicator: Image {
|
||||||
id: dropdownIcon
|
id: dropdownIcon
|
||||||
|
|
||||||
x: control.width - width - 10
|
x: control.width - width - 10
|
||||||
y: control.topPadding + (control.availableHeight - height) / 2
|
y: control.topPadding + (control.availableHeight - height) / 2
|
||||||
|
|
||||||
width: 12
|
width: 12
|
||||||
height: 8
|
height: 8
|
||||||
source: palette.window.hslLightness > 0.5
|
source: palette.window.hslLightness > 0.5
|
||||||
@@ -92,7 +95,7 @@ Basic.ComboBox {
|
|||||||
|
|
||||||
popup: Popup {
|
popup: Popup {
|
||||||
y: control.height + 2
|
y: control.height + 2
|
||||||
width: control.width
|
width: Math.max(control.width, control.popupContentWidth)
|
||||||
implicitHeight: Math.min(contentItem.implicitHeight, 300)
|
implicitHeight: Math.min(contentItem.implicitHeight, 300)
|
||||||
padding: 4
|
padding: 4
|
||||||
|
|
||||||
@@ -101,6 +104,8 @@ Basic.ComboBox {
|
|||||||
implicitHeight: contentHeight
|
implicitHeight: contentHeight
|
||||||
model: control.popup.visible ? control.delegateModel : null
|
model: control.popup.visible ? control.delegateModel : null
|
||||||
currentIndex: control.highlightedIndex
|
currentIndex: control.highlightedIndex
|
||||||
|
boundsBehavior: ListView.StopAtBounds
|
||||||
|
highlightMoveDuration: 0
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
policy: ScrollBar.AsNeeded
|
policy: ScrollBar.AsNeeded
|
||||||
@@ -124,7 +129,7 @@ Basic.ComboBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
delegate: ItemDelegate {
|
||||||
width: control.width - 8
|
width: control.popup.width - 8
|
||||||
height: 32
|
height: 32
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
@@ -153,5 +158,10 @@ Basic.ComboBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: textMetrics
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
UIControls/qml/QoASeparator.qml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
height: 15
|
||||||
|
width: 1
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls.Basic
|
import QtQuick.Controls.Basic
|
||||||
|
|||||||
68
UIControls/qml/QoAToolTip.qml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
ToolTip {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
padding: 8
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: root.text
|
||||||
|
font: root.font
|
||||||
|
color: palette.toolTipText
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Item {
|
||||||
|
implicitWidth: bg.implicitWidth
|
||||||
|
implicitHeight: bg.implicitHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: bg
|
||||||
|
anchors.margins: -2
|
||||||
|
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.12)
|
||||||
|
radius: 8
|
||||||
|
z: -2
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: bg
|
||||||
|
anchors.margins: -1
|
||||||
|
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.08)
|
||||||
|
radius: 7
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bg
|
||||||
|
anchors.fill: parent
|
||||||
|
color: palette.toolTipBase
|
||||||
|
border.color: Qt.darker(palette.toolTipBase, 1.2)
|
||||||
|
border.width: 1
|
||||||
|
radius: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enter: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
from: 0.0
|
||||||
|
to: 1.0
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
from: 1.0
|
||||||
|
to: 0.0
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.InQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "UpdateStatusWidget.hpp"
|
#include "UpdateStatusWidget.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* QodeAssist is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* QodeAssist is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||