mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-13 09:49:12 -04:00
Compare commits
54 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 |
14
.github/workflows/build_cmake.yml
vendored
14
.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
|
||||||
|
|||||||
2
.gitignore
vendored
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
3
.gitmodules
vendored
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "sources/external/llmqore"]
|
||||||
|
path = sources/external/llmqore
|
||||||
|
url = https://github.com/Palm1r/llmqore.git
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -61,6 +62,8 @@ add_qtc_plugin(QodeAssist
|
|||||||
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
|
||||||
@@ -93,10 +96,12 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/OpenAIResponses.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
|
||||||
@@ -112,7 +117,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
providers/OpenAIResponses/ItemTypesReference.hpp
|
providers/OpenAIResponses/ItemTypesReference.hpp
|
||||||
providers/OpenAIResponsesRequestBuilder.hpp
|
providers/OpenAIResponsesRequestBuilder.hpp
|
||||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
||||||
providers/OpenAIResponsesMessage.hpp providers/OpenAIResponsesMessage.cpp
|
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
@@ -141,23 +145,22 @@ add_qtc_plugin(QodeAssist
|
|||||||
widgets/DiffStatistics.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/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,6 +44,7 @@ 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
|
||||||
@@ -52,6 +54,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/tools-icon-on.svg
|
icons/tools-icon-on.svg
|
||||||
icons/tools-icon-off.svg
|
icons/tools-icon-off.svg
|
||||||
icons/settings-icon.svg
|
icons/settings-icon.svg
|
||||||
|
icons/compress-icon.svg
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
@@ -65,6 +68,8 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
ChatData.hpp
|
ChatData.hpp
|
||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
ChatFileManager.hpp ChatFileManager.cpp
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
|
FileMentionItem.hpp FileMentionItem.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
@@ -75,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
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
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
|
||||||
|
|
||||||
|
|||||||
@@ -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 "ChatFileManager.hpp"
|
#include "ChatFileManager.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.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 "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include <utils/aspects.h>
|
#include <utils/aspects.h>
|
||||||
@@ -117,8 +101,10 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||||
QString fullPath = QDir(contentFolder).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);
|
||||||
|
|||||||
@@ -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,29 +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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ChatRootView.hpp"
|
#include "ChatRootView.hpp"
|
||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <coreplugin/icore.h>
|
#include <coreplugin/icore.h>
|
||||||
@@ -34,7 +21,9 @@
|
|||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
|
#include "AgentRole.hpp"
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "ChatCompressor.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "ConfigurationManager.hpp"
|
#include "ConfigurationManager.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
@@ -46,17 +35,18 @@
|
|||||||
#include "context/ChangesManager.h"
|
#include "context/ChangesManager.h"
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
#include "context/TokenUtils.hpp"
|
#include "context/TokenUtils.hpp"
|
||||||
#include "llmcore/RulesLoader.hpp"
|
#include "pluginllmcore/RulesLoader.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatRootView::ChatRootView(QQuickItem *parent)
|
ChatRootView::ChatRootView(QQuickItem *parent)
|
||||||
: QQuickItem(parent)
|
: QQuickItem(parent)
|
||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
||||||
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
||||||
, m_fileManager(new ChatFileManager(this))
|
, m_fileManager(new ChatFileManager(this))
|
||||||
, m_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
|
, m_chatCompressor(new ChatCompressor(this))
|
||||||
{
|
{
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(
|
||||||
@@ -117,6 +107,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::updateInputTokensCount);
|
&ChatRootView::updateInputTokensCount);
|
||||||
|
connect(
|
||||||
|
&Settings::chatAssistantSettings().systemPrompt,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::baseSystemPromptChanged);
|
||||||
|
|
||||||
auto editors = Core::EditorManager::instance();
|
auto editors = Core::EditorManager::instance();
|
||||||
|
|
||||||
@@ -209,6 +204,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
refreshRules();
|
refreshRules();
|
||||||
loadAvailableConfigurations();
|
loadAvailableConfigurations();
|
||||||
|
loadAvailableAgentRoles();
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
@@ -216,6 +212,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::refreshRules);
|
&ChatRootView::refreshRules);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
|
&ProjectExplorer::ProjectManager::projectAdded,
|
||||||
|
this,
|
||||||
|
&ChatRootView::openFilesChanged);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
|
&ProjectExplorer::ProjectManager::projectRemoved,
|
||||||
|
this,
|
||||||
|
&ChatRootView::openFilesChanged);
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&Settings::chatAssistantSettings().enableChatTools,
|
&Settings::chatAssistantSettings().enableChatTools,
|
||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
@@ -238,6 +246,27 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
m_lastErrorMessage = error;
|
m_lastErrorMessage = error;
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ChatCompressor signals
|
||||||
|
connect(m_chatCompressor, &ChatCompressor::compressionStarted, this, [this]() {
|
||||||
|
emit isCompressingChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_chatCompressor, &ChatCompressor::compressionCompleted, this, [this](const QString &compressedChatPath) {
|
||||||
|
emit isCompressingChanged();
|
||||||
|
m_lastInfoMessage = tr("Chat compressed successfully!");
|
||||||
|
emit lastInfoMessageChanged();
|
||||||
|
emit compressionCompleted(compressedChatPath);
|
||||||
|
|
||||||
|
loadHistory(compressedChatPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_chatCompressor, &ChatCompressor::compressionFailed, this, [this](const QString &error) {
|
||||||
|
emit isCompressingChanged();
|
||||||
|
m_lastErrorMessage = error;
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
emit compressionFailed(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel *ChatRootView::chatModel() const
|
ChatModel *ChatRootView::chatModel() const
|
||||||
@@ -311,6 +340,12 @@ void ChatRootView::clearLinkedFiles()
|
|||||||
emit linkedFilesChanged();
|
emit linkedFilesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatRootView::clearMessages()
|
||||||
|
{
|
||||||
|
m_clientInterface->clearMessages();
|
||||||
|
clearLinkedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
QString ChatRootView::getChatsHistoryDir() const
|
QString ChatRootView::getChatsHistoryDir() const
|
||||||
{
|
{
|
||||||
QString path;
|
QString path;
|
||||||
@@ -699,7 +734,17 @@ void ChatRootView::openRulesFolder()
|
|||||||
|
|
||||||
void ChatRootView::openSettings()
|
void ChatRootView::openSettings()
|
||||||
{
|
{
|
||||||
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID);
|
QMetaObject::invokeMethod(
|
||||||
|
this,
|
||||||
|
[]() { Settings::showSettings(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID); },
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::openFileInEditor(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::updateInputTokensCount()
|
void ChatRootView::updateInputTokensCount()
|
||||||
@@ -752,6 +797,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
|||||||
if (editor) {
|
if (editor) {
|
||||||
m_currentEditors.removeOne(editor);
|
m_currentEditors.removeOne(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit openFilesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
||||||
@@ -769,6 +816,7 @@ void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath
|
|||||||
{
|
{
|
||||||
if (editor && editor->document()) {
|
if (editor && editor->document()) {
|
||||||
m_currentEditors.append(editor);
|
m_currentEditors.append(editor);
|
||||||
|
emit openFilesChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,7 +913,7 @@ QString ChatRootView::getRuleContent(int index)
|
|||||||
if (index < 0 || index >= m_activeRules.size())
|
if (index < 0 || index >= m_activeRules.size())
|
||||||
return QString();
|
return QString();
|
||||||
|
|
||||||
return LLMCore::RulesLoader::loadRuleFileContent(
|
return PluginLLMCore::RulesLoader::loadRuleFileContent(
|
||||||
m_activeRules[index].toMap()["filePath"].toString());
|
m_activeRules[index].toMap()["filePath"].toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +921,7 @@ void ChatRootView::refreshRules()
|
|||||||
{
|
{
|
||||||
m_activeRules.clear();
|
m_activeRules.clear();
|
||||||
|
|
||||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||||
if (!project) {
|
if (!project) {
|
||||||
emit activeRulesChanged();
|
emit activeRulesChanged();
|
||||||
emit activeRulesCountChanged();
|
emit activeRulesCountChanged();
|
||||||
@@ -881,7 +929,7 @@ void ChatRootView::refreshRules()
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto ruleFiles
|
auto ruleFiles
|
||||||
= LLMCore::RulesLoader::getRuleFilesForProject(project, LLMCore::RulesContext::Chat);
|
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||||
|
|
||||||
for (const auto &ruleFile : ruleFiles) {
|
for (const auto &ruleFile : ruleFiles) {
|
||||||
QVariantMap ruleMap;
|
QVariantMap ruleMap;
|
||||||
@@ -1232,9 +1280,9 @@ QString ChatRootView::lastInfoMessage() const
|
|||||||
bool ChatRootView::isThinkingSupport() const
|
bool ChatRootView::isThinkingSupport() const
|
||||||
{
|
{
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
auto providerName = Settings::generalSettings().caProvider();
|
||||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
return provider && provider->supportThinking();
|
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::generateChatFileName(const QString &shortMessage, const QString &dir) const
|
QString ChatRootView::generateChatFileName(const QString &shortMessage, const QString &dir) const
|
||||||
@@ -1336,8 +1384,6 @@ void ChatRootView::applyConfiguration(const QString &configName)
|
|||||||
settings.caModel.setValue(config.model);
|
settings.caModel.setValue(config.model);
|
||||||
settings.caTemplate.setValue(config.templateName);
|
settings.caTemplate.setValue(config.templateName);
|
||||||
settings.caUrl.setValue(config.url);
|
settings.caUrl.setValue(config.url);
|
||||||
settings.caEndpointMode.setValue(
|
|
||||||
settings.caEndpointMode.indexForDisplay(config.endpointMode));
|
|
||||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
||||||
|
|
||||||
settings.writeSettings();
|
settings.writeSettings();
|
||||||
@@ -1360,4 +1406,130 @@ QString ChatRootView::currentConfiguration() const
|
|||||||
return m_currentConfiguration;
|
return m_currentConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatRootView::loadAvailableAgentRoles()
|
||||||
|
{
|
||||||
|
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||||
|
|
||||||
|
m_availableAgentRoles.clear();
|
||||||
|
m_availableAgentRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
||||||
|
|
||||||
|
for (const auto &role : roles)
|
||||||
|
m_availableAgentRoles.append(role.name);
|
||||||
|
|
||||||
|
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||||
|
m_currentAgentRole = Settings::AgentRolesManager::getNoRole().name;
|
||||||
|
|
||||||
|
if (!lastRoleId.isEmpty()) {
|
||||||
|
for (const auto &role : roles) {
|
||||||
|
if (role.id == lastRoleId) {
|
||||||
|
m_currentAgentRole = role.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit availableAgentRolesChanged();
|
||||||
|
emit currentAgentRoleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::applyAgentRole(const QString &roleName)
|
||||||
|
{
|
||||||
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
|
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
||||||
|
settings.lastUsedRoleId.setValue("");
|
||||||
|
settings.writeSettings();
|
||||||
|
m_currentAgentRole = roleName;
|
||||||
|
emit currentAgentRoleChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||||
|
|
||||||
|
for (const auto &role : roles) {
|
||||||
|
if (role.name == roleName) {
|
||||||
|
settings.lastUsedRoleId.setValue(role.id);
|
||||||
|
settings.writeSettings();
|
||||||
|
m_currentAgentRole = role.name;
|
||||||
|
emit currentAgentRoleChanged();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatRootView::availableAgentRoles() const
|
||||||
|
{
|
||||||
|
return m_availableAgentRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::currentAgentRole() const
|
||||||
|
{
|
||||||
|
return m_currentAgentRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::baseSystemPrompt() const
|
||||||
|
{
|
||||||
|
return Settings::chatAssistantSettings().systemPrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::currentAgentRoleDescription() const
|
||||||
|
{
|
||||||
|
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||||
|
if (lastRoleId.isEmpty())
|
||||||
|
return Settings::AgentRolesManager::getNoRole().description;
|
||||||
|
|
||||||
|
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||||
|
if (role.id.isEmpty())
|
||||||
|
return Settings::AgentRolesManager::getNoRole().description;
|
||||||
|
|
||||||
|
return role.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
||||||
|
{
|
||||||
|
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||||
|
if (lastRoleId.isEmpty())
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||||
|
if (role.id.isEmpty())
|
||||||
|
return QString();
|
||||||
|
|
||||||
|
return role.systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::openAgentRolesSettings()
|
||||||
|
{
|
||||||
|
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::compressCurrentChat()
|
||||||
|
{
|
||||||
|
if (m_chatCompressor->isCompressing()) {
|
||||||
|
m_lastErrorMessage = tr("Compression is already in progress");
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_recentFilePath.isEmpty()) {
|
||||||
|
m_lastErrorMessage = tr("No chat file to compress. Please save the chat first.");
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autosave();
|
||||||
|
|
||||||
|
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::cancelCompression()
|
||||||
|
{
|
||||||
|
m_chatCompressor->cancelCompression();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatRootView::isCompressing() const
|
||||||
|
{
|
||||||
|
return m_chatCompressor->isCompressing();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,34 +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 "ChatFileManager.hpp"
|
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||||
#include "llmcore/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
|
||||||
@@ -59,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
|
||||||
|
|
||||||
@@ -96,6 +89,8 @@ public:
|
|||||||
Q_INVOKABLE void openRulesFolder();
|
Q_INVOKABLE void openRulesFolder();
|
||||||
Q_INVOKABLE void openSettings();
|
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;
|
||||||
|
|
||||||
@@ -145,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;
|
||||||
@@ -154,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);
|
||||||
@@ -161,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();
|
||||||
@@ -191,6 +201,16 @@ signals:
|
|||||||
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;
|
||||||
@@ -199,7 +219,7 @@ 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;
|
ChatFileManager *m_fileManager;
|
||||||
QString m_currentTemplate;
|
QString m_currentTemplate;
|
||||||
@@ -223,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"
|
||||||
@@ -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 contentFolder = getChatContentFolder(filePath);
|
|
||||||
QDir dir;
|
|
||||||
if (!dir.exists(contentFolder)) {
|
|
||||||
if (!dir.mkpath(contentFolder)) {
|
|
||||||
LOG_MESSAGE(QString("Warning: Failed to create content folder: %1").arg(contentFolder));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,21 +64,22 @@ 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;
|
||||||
|
|
||||||
if (message.isRedacted) {
|
if (message.isRedacted) {
|
||||||
messageObj["isRedacted"] = true;
|
messageObj["isRedacted"] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.signature.isEmpty()) {
|
if (!message.signature.isEmpty()) {
|
||||||
messageObj["signature"] = message.signature;
|
messageObj["signature"] = message.signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.attachments.isEmpty()) {
|
if (!message.attachments.isEmpty()) {
|
||||||
QJsonArray attachmentsArray;
|
QJsonArray attachmentsArray;
|
||||||
for (const auto &attachment : message.attachments) {
|
for (const auto &attachment : message.attachments) {
|
||||||
@@ -113,7 +90,7 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
|
|||||||
}
|
}
|
||||||
messageObj["attachments"] = attachmentsArray;
|
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) {
|
||||||
@@ -125,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());
|
||||||
@@ -137,7 +115,7 @@ 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")) {
|
if (json.contains("attachments")) {
|
||||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
QJsonArray attachmentsArray = json["attachments"].toArray();
|
||||||
for (const auto &attachmentValue : attachmentsArray) {
|
for (const auto &attachmentValue : attachmentsArray) {
|
||||||
@@ -148,7 +126,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
|||||||
message.attachments.append(attachment);
|
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) {
|
||||||
@@ -160,7 +138,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
|||||||
message.images.append(image);
|
message.images.append(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,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;
|
||||||
@@ -189,17 +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, message.isRedacted, message.signature);
|
model->addMessage(
|
||||||
|
message.content,
|
||||||
|
message.role,
|
||||||
|
message.id,
|
||||||
|
message.attachments,
|
||||||
|
message.images,
|
||||||
|
message.isRedacted,
|
||||||
|
message.signature);
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
||||||
.arg(message.images.size())
|
.arg(message.images.size())
|
||||||
.arg(message.isRedacted)
|
.arg(message.isRedacted)
|
||||||
.arg(message.signature.length()));
|
.arg(message.signature.length()));
|
||||||
}
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(false);
|
model->setLoadingFromHistory(false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -217,12 +203,14 @@ bool ChatSerializer::validateVersion(const QString &version)
|
|||||||
if (version == VERSION) {
|
if (version == VERSION) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version == "0.1") {
|
if (version == "0.1") {
|
||||||
LOG_MESSAGE("Loading chat from old format 0.1 - images folder structure has changed from _images to _content");
|
LOG_MESSAGE(
|
||||||
|
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
||||||
|
"to _content");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,10 +222,11 @@ QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
|||||||
return QDir(dirPath).filePath(baseName + "_content");
|
return QDir(dirPath).filePath(baseName + "_content");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::saveContentToStorage(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 contentFolder = getChatContentFolder(chatFilePath);
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
QDir dir;
|
QDir dir;
|
||||||
@@ -247,34 +236,34 @@ bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
|
|||||||
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(contentFolder).filePath(uniqueName);
|
QString fullPath = QDir(contentFolder).filePath(uniqueName);
|
||||||
|
|
||||||
QByteArray contentData = 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(contentData) == -1) {
|
if (file.write(contentData) == -1) {
|
||||||
LOG_MESSAGE(QString("Failed to write content 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 content: %1 to %2").arg(fileName, fullPath));
|
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,16 +271,16 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
|
|||||||
{
|
{
|
||||||
QString contentFolder = getChatContentFolder(chatFilePath);
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
QString fullPath = QDir(contentFolder).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 content file: %1").arg(fullPath));
|
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray contentData = file.readAll();
|
QByteArray contentData = file.readAll();
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
return contentData.toBase64();
|
return contentData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 "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,10 @@
|
|||||||
/*
|
// 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/buildconfiguration.h>
|
||||||
#include <projectexplorer/target.h>
|
#include <projectexplorer/target.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
@@ -40,12 +26,15 @@
|
|||||||
#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 "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
#include "RequestConfig.hpp"
|
|
||||||
#include "ToolsSettings.hpp"
|
#include "ToolsSettings.hpp"
|
||||||
#include <RulesLoader.hpp>
|
#include <RulesLoader.hpp>
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
@@ -53,7 +42,7 @@
|
|||||||
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)
|
||||||
@@ -138,7 +127,7 @@ void ClientInterface::sendMessage(
|
|||||||
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));
|
||||||
@@ -153,14 +142,21 @@ void ClientInterface::sendMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData context;
|
PluginLLMCore::ContextData context;
|
||||||
|
|
||||||
const bool isToolsEnabled = useTools;
|
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());
|
||||||
@@ -170,12 +166,12 @@ void ClientInterface::sendMessage(
|
|||||||
if (auto target = project->activeTarget()) {
|
if (auto target = project->activeTarget()) {
|
||||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||||
systemPrompt += QString("\n# Active Build directory: %1")
|
systemPrompt += QString("\n# Active Build directory: %1")
|
||||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
.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;
|
||||||
@@ -190,13 +186,13 @@ 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;
|
||||||
|
|
||||||
@@ -216,7 +212,8 @@ void ClientInterface::sendMessage(
|
|||||||
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;
|
||||||
@@ -226,109 +223,105 @@ void ClientInterface::sendMessage(
|
|||||||
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,
|
||||||
useTools,
|
useTools,
|
||||||
useThinking);
|
useThinking);
|
||||||
|
|
||||||
QString requestId = QUuid::createUuid().toString();
|
provider->client()->setMaxToolContinuations(
|
||||||
|
Settings::toolsSettings().maxToolContinuations());
|
||||||
|
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::chunkReceived,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handlePartialResponse,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleFullResponse,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleRequestFailed,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::toolStarted,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleToolExecutionStarted,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::toolResultReady,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleToolExecutionCompleted,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::thinkingBlockReceived,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleThinkingBlockReceived,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
|
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||||
|
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||||
|
: promptTemplate->endpoint();
|
||||||
|
auto requestId
|
||||||
|
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||||
QJsonObject request{{"id", requestId}};
|
QJsonObject request{{"id", requestId}};
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
emit requestStarted(requestId);
|
||||||
|
|
||||||
connect(
|
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||||
provider,
|
&& provider->toolsManager()) {
|
||||||
&LLMCore::Provider::partialResponseReceived,
|
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||||
this,
|
provider->toolsManager()->tool("todo_tool"))) {
|
||||||
&ClientInterface::handlePartialResponse,
|
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||||
Qt::UniqueConnection);
|
}
|
||||||
connect(
|
}
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::fullResponseReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleFullResponse,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::requestFailed,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleRequestFailed,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::toolExecutionStarted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleToolExecutionStarted,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::toolExecutionCompleted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleToolExecutionCompleted,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::continuationStarted,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleCleanAccumulatedData,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::thinkingBlockReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleThinkingBlockReceived,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
|
||||||
provider,
|
|
||||||
&LLMCore::Provider::redactedThinkingBlockReceived,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleRedactedThinkingBlockReceived,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -336,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) {
|
||||||
@@ -348,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");
|
||||||
}
|
}
|
||||||
@@ -414,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();
|
||||||
@@ -444,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)
|
||||||
@@ -461,18 +458,9 @@ 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);
|
||||||
if (m_accumulatedResponses.contains(requestId)) {
|
|
||||||
m_accumulatedResponses.remove(requestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
|
|
||||||
{
|
|
||||||
m_accumulatedResponses[requestId].clear();
|
|
||||||
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleThinkingBlockReceived(
|
void ClientInterface::handleThinkingBlockReceived(
|
||||||
@@ -483,19 +471,17 @@ void ClientInterface::handleThinkingBlockReceived(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
if (m_awaitingContinuation.remove(requestId)) {
|
||||||
}
|
m_accumulatedResponses[requestId].clear();
|
||||||
|
|
||||||
void ClientInterface::handleRedactedThinkingBlockReceived(
|
|
||||||
const QString &requestId, const QString &signature)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId)) {
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
QString("Ignoring redacted thinking block for non-chat request: %1").arg(requestId));
|
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
if (thinking.isEmpty()) {
|
||||||
|
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
||||||
|
} else {
|
||||||
|
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionStarted(
|
void ClientInterface::handleToolExecutionStarted(
|
||||||
@@ -507,6 +493,7 @@ void ClientInterface::handleToolExecutionStarted(
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
||||||
|
m_awaitingContinuation.insert(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionCompleted(
|
void ClientInterface::handleToolExecutionCompleted(
|
||||||
@@ -570,10 +557,10 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
|||||||
return imageData.toBase64();
|
return imageData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||||
const QList<ChatModel::ImageAttachment> &storedImages) const
|
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
|
QString base64Data
|
||||||
@@ -583,7 +570,7 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
|||||||
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;
|
||||||
@@ -596,6 +583,20 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
|||||||
|
|
||||||
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,7 +21,7 @@ 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(
|
||||||
@@ -62,10 +47,8 @@ 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(
|
void handleThinkingBlockReceived(
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
const QString &requestId, const QString &thinking, const QString &signature);
|
||||||
void handleRedactedThinkingBlockReceived(const QString &requestId, const QString &signature);
|
|
||||||
void handleToolExecutionStarted(
|
void handleToolExecutionStarted(
|
||||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||||
void handleToolExecutionCompleted(
|
void handleToolExecutionCompleted(
|
||||||
@@ -82,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
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
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
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
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 |
@@ -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
|
||||||
@@ -77,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
|
||||||
|
|
||||||
@@ -97,8 +97,7 @@ 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
|
||||||
@@ -131,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
|
||||||
@@ -147,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
|
||||||
|
|
||||||
@@ -167,11 +190,6 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (componentLoader.sourceComponent == chatItemComponent) {
|
|
||||||
chatListView.hideServiceComponents(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header: Item {
|
header: Item {
|
||||||
@@ -183,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,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
|
||||||
@@ -281,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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,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
|
||||||
@@ -417,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)
|
||||||
@@ -438,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()
|
||||||
}
|
}
|
||||||
@@ -455,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
|
||||||
@@ -479,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 {
|
||||||
@@ -506,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
|
||||||
@@ -180,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 {
|
||||||
@@ -204,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
|
||||||
}
|
}
|
||||||
@@ -257,33 +254,21 @@ Rectangle {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
onClicked: (mouse) => {
|
onClicked: (mouse) => {
|
||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
fileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
fileItem.openFileInEditor()
|
fileItem.openFileInEditor()
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
attachmentContextMenu.popup()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.visible: containsMouse
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Left click: Open in Qt Creator\nRight click: More options")
|
visible: attachFileMouseArea.containsMouse
|
||||||
ToolTip.delay: 500
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
}
|
delay: 500
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: attachmentContextMenu
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Open in Qt Creator")
|
|
||||||
onTriggered: fileItem.openFileInEditor()
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Open in System Editor")
|
|
||||||
onTriggered: fileItem.openFileInExternalEditor()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +290,7 @@ Rectangle {
|
|||||||
|
|
||||||
FileItem {
|
FileItem {
|
||||||
id: imageFileItem
|
id: imageFileItem
|
||||||
filePath: itemData.imageUrl ? itemData.imageUrl.toString().replace("file://", "") : ""
|
filePath: itemData.filePath || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -361,33 +346,21 @@ Rectangle {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
onClicked: (mouse) => {
|
onClicked: (mouse) => {
|
||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
imageFileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
imageFileItem.openFileInEditor()
|
imageFileItem.openFileInEditor()
|
||||||
} else if (mouse.button === Qt.RightButton) {
|
|
||||||
imageContextMenu.popup()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.visible: containsMouse
|
QoAToolTip {
|
||||||
ToolTip.text: qsTr("Left click: Open in System\nRight click: More options")
|
visible: imageMouseArea.containsMouse
|
||||||
ToolTip.delay: 500
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
}
|
delay: 500
|
||||||
|
|
||||||
Menu {
|
|
||||||
id: imageContextMenu
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Open in Qt Creator")
|
|
||||||
onTriggered: imageFileItem.openFileInEditor()
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuItem {
|
|
||||||
text: qsTr("Open in System Viewer")
|
|
||||||
onTriggered: imageFileItem.openFileInExternalEditor()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
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
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
|
||||||
|
|||||||
@@ -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,12 +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 toolsButton: toolsButtonId
|
property alias toolsButton: toolsButtonId
|
||||||
property alias thinkingMode: thinkingModeId
|
property alias thinkingMode: thinkingModeId
|
||||||
property alias settingsButton: settingsButtonId
|
property alias settingsButton: settingsButtonId
|
||||||
property alias activeRulesCount: activeRulesCountId.text
|
|
||||||
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) :
|
||||||
@@ -87,9 +71,26 @@ Rectangle {
|
|||||||
|
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: qsTr("Switch AI configuration")
|
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 {
|
QoAButton {
|
||||||
id: toolsButtonId
|
id: toolsButtonId
|
||||||
|
|
||||||
@@ -157,8 +158,13 @@ Rectangle {
|
|||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoASeparator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
height: firstRow.height
|
height: firstRow.height
|
||||||
width: recentPathId.width
|
width: recentPathId.width
|
||||||
@@ -217,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
|
||||||
|
|
||||||
@@ -243,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 {
|
||||||
@@ -282,6 +260,21 @@ Rectangle {
|
|||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoASeparator {}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: clearButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Clean chat")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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,8 +107,6 @@ 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") {
|
||||||
handleCancelRequest();
|
handleCancelRequest();
|
||||||
@@ -136,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);
|
||||||
@@ -144,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) {
|
||||||
@@ -270,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;
|
||||||
@@ -314,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});
|
||||||
}
|
}
|
||||||
@@ -328,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(
|
||||||
@@ -338,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();
|
||||||
@@ -395,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.4",
|
"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.
|
||||||
*
|
*
|
||||||
@@ -54,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)
|
||||||
@@ -69,10 +153,6 @@ 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);
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
@@ -108,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;
|
||||||
@@ -131,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()) {
|
||||||
@@ -170,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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,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{
|
||||||
@@ -270,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,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());
|
||||||
@@ -325,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();
|
||||||
@@ -338,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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,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);
|
||||||
}
|
}
|
||||||
@@ -423,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();
|
||||||
@@ -465,13 +545,6 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
|
|
||||||
{
|
|
||||||
return Utils::Text::Position{pos.line, pos.column};
|
|
||||||
}
|
|
||||||
} // anonymous namespace
|
|
||||||
|
|
||||||
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
|
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
|
||||||
{
|
{
|
||||||
TextEditorWidget *editorWidget = result.editor;
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
@@ -604,58 +677,20 @@ void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor
|
|||||||
editCursor.endEditBlock();
|
editCursor.endEditBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
|
||||||
int charsAdded,
|
|
||||||
bool isSpaceOrTab)
|
|
||||||
{
|
{
|
||||||
Q_UNUSED(isSpaceOrTab);
|
const QTextCursor cursor = widget->textCursor();
|
||||||
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
const bool smart = settings.smartContextTrigger();
|
||||||
|
|
||||||
if (m_recentCharCount
|
if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
return;
|
||||||
|
|
||||||
|
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
|
||||||
|
const int charThreshold = settings.autoCompletionCharThreshold();
|
||||||
|
|
||||||
|
if (eager || m_recentCharCount > charThreshold)
|
||||||
scheduleRequest(widget);
|
scheduleRequest(widget);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QodeAssistClient::handleHintBasedTrigger(TextEditor::TextEditorWidget *widget,
|
|
||||||
int charsAdded,
|
|
||||||
bool isSpaceOrTab,
|
|
||||||
QTextCursor &cursor)
|
|
||||||
{
|
|
||||||
Q_UNUSED(charsAdded);
|
|
||||||
|
|
||||||
const int hintThreshold = Settings::codeCompletionSettings().hintCharThreshold();
|
|
||||||
if (m_recentCharCount >= hintThreshold && !isSpaceOrTab) {
|
|
||||||
const QRect cursorRect = widget->cursorRect(cursor);
|
|
||||||
QPoint globalPos = widget->viewport()->mapToGlobal(cursorRect.topLeft());
|
|
||||||
QPoint localPos = widget->mapFromGlobal(globalPos);
|
|
||||||
|
|
||||||
int fontSize = widget->font().pixelSize();
|
|
||||||
if (fontSize <= 0) {
|
|
||||||
fontSize = widget->fontMetrics().height();
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextCursor textCursor = widget->textCursor();
|
|
||||||
|
|
||||||
if (m_recentCharCount <= hintThreshold) {
|
|
||||||
textCursor
|
|
||||||
.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, m_recentCharCount);
|
|
||||||
} else {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||||
@@ -667,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);
|
||||||
@@ -724,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,12 +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 "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 {
|
||||||
|
|
||||||
@@ -54,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;
|
||||||
@@ -75,8 +51,7 @@ private:
|
|||||||
void displayRefactoringWidget(const RefactorResult &result);
|
void displayRefactoringWidget(const RefactorResult &result);
|
||||||
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
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;
|
||||||
@@ -85,10 +60,8 @@ 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};
|
||||||
|
|||||||
@@ -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,40 +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 <llmcore/ResponseCleaner.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 {
|
||||||
|
|
||||||
@@ -109,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);
|
||||||
@@ -140,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;
|
||||||
@@ -287,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;
|
||||||
@@ -368,7 +340,7 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
|
|
||||||
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."
|
||||||
@@ -387,7 +359,7 @@ void QuickRefactorHandler::handleLLMResponse(
|
|||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
QString cleanedResponse = LLMCore::ResponseCleaner::clean(response);
|
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
166
README.md
166
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`
|
||||||
|
|
||||||
@@ -176,6 +223,8 @@ 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
|
||||||
@@ -188,10 +237,37 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
|||||||
- **[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
|
||||||
|
|
||||||
@@ -260,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] │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -321,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
|
||||||
@@ -328,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 |
|
||||||
@@ -370,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:
|
||||||
@@ -396,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
|
||||||
|
|||||||
@@ -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) 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
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,9 +8,26 @@ 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
|
||||||
|
|
||||||
@@ -94,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
|
||||||
|
|
||||||
@@ -128,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 {
|
||||||
@@ -157,5 +158,10 @@ Basic.ComboBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: textMetrics
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
UIControls/qml/QoASeparator.qml
Normal file
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
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
|
||||||
|
|
||||||
|
|||||||
20
Version.hpp
20
Version.hpp
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#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 "ChatOutputPane.h"
|
#include "ChatOutputPane.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 "NavigationPanel.hpp"
|
#include "NavigationPanel.hpp"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user