Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a2ba08538 | ||
|
|
37084bec59 | ||
|
|
6910037e97 | ||
|
|
a72cdd85a4 | ||
|
|
31b4e73af5 | ||
|
|
088887c802 | ||
|
|
b7a9787cc3 | ||
|
|
e2e13f0f38 | ||
|
|
49ae335d7d | ||
|
|
2ba58a403f | ||
|
|
3de1619bf0 | ||
|
|
ec45067336 | ||
|
|
52fb65c5b1 | ||
|
|
478f369ad2 | ||
|
|
762c965377 | ||
|
|
d2b93310e2 | ||
|
|
f3b1e7f411 | ||
|
|
a55c6ccfdb | ||
|
|
b32433c336 | ||
|
|
6f11260cd1 | ||
|
|
ddd6aba091 | ||
|
|
e3f464c54e | ||
|
|
e86e58337a | ||
|
|
dbd47387be | ||
|
|
50e1276ab2 | ||
|
|
50c948ccfe | ||
|
|
949dad4fd2 | ||
|
|
01fd7dad6f | ||
|
|
fd408ba415 | ||
|
|
14e7ea2ec3 | ||
|
|
9f050aec67 | ||
|
|
9e118ddfaf | ||
|
|
157498b770 | ||
|
|
5c8a8f305d | ||
|
|
fc33bb60d0 | ||
|
|
498eb4d932 | ||
|
|
fb941cea99 | ||
|
|
a0af983bda | ||
|
|
4bd96e0718 | ||
|
|
7b0d3c2abb | ||
|
|
75d1551b00 | ||
|
|
406ba05bfb | ||
|
|
7a97d0aba5 | ||
|
|
b19c4c0c0c | ||
|
|
a466332822 | ||
|
|
e1fa01d123 | ||
|
|
37e41d3b76 | ||
|
|
2d5667d8ca | ||
|
|
22377c8f6a | ||
|
|
595895840a | ||
|
|
f6d647d5c8 | ||
|
|
1f9c60ffb2 | ||
|
|
6ec4a61c0c | ||
|
|
7feb088de3 | ||
|
|
627a821115 | ||
|
|
9b0ae98f02 | ||
|
|
85a7bba90e | ||
|
|
b18ef4c400 | ||
|
|
bbacdfc22a | ||
|
|
670f81c3dd | ||
|
|
b4f31dee23 | ||
|
|
dc6ec4fb4f | ||
|
|
07de415346 | ||
|
|
a15f64a234 |
14
.github/workflows/build_cmake.yml
vendored
@@ -46,16 +46,12 @@ 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:
|
||||||
@@ -165,7 +161,7 @@ jobs:
|
|||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
foreach(package qtbase qtdeclarative qttools)
|
foreach(package qtbase qtdeclarative qttools qtsvg)
|
||||||
downloadAndExtract(
|
downloadAndExtract(
|
||||||
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
|
||||||
${package}.7z
|
${package}.7z
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
find_package(QtCreator REQUIRED COMPONENTS Core)
|
find_package(QtCreator REQUIRED COMPONENTS Core)
|
||||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED)
|
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
|
||||||
find_package(GTest)
|
find_package(GTest)
|
||||||
|
|
||||||
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
|
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
|
||||||
@@ -57,6 +57,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::Widgets
|
Qt::Widgets
|
||||||
Qt::Network
|
Qt::Network
|
||||||
|
Qt::Svg
|
||||||
QtCreator::ExtensionSystem
|
QtCreator::ExtensionSystem
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
QtCreator::CPlusPlus
|
QtCreator::CPlusPlus
|
||||||
@@ -69,6 +70,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssistConstants.hpp
|
QodeAssistConstants.hpp
|
||||||
QodeAssisttr.h
|
QodeAssisttr.h
|
||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
|
RefactorContextHelper.hpp
|
||||||
templates/Templates.hpp
|
templates/Templates.hpp
|
||||||
templates/CodeLlamaFim.hpp
|
templates/CodeLlamaFim.hpp
|
||||||
templates/Ollama.hpp
|
templates/Ollama.hpp
|
||||||
@@ -88,6 +90,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/GoogleAI.hpp
|
templates/GoogleAI.hpp
|
||||||
templates/LlamaCppFim.hpp
|
templates/LlamaCppFim.hpp
|
||||||
templates/Qwen3CoderFIM.hpp
|
templates/Qwen3CoderFIM.hpp
|
||||||
|
templates/OpenAIResponses.hpp
|
||||||
providers/Providers.hpp
|
providers/Providers.hpp
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||||
@@ -99,6 +102,17 @@ add_qtc_plugin(QodeAssist
|
|||||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
||||||
|
providers/OpenAIResponses/ModelRequest.hpp
|
||||||
|
providers/OpenAIResponses/ResponseObject.hpp
|
||||||
|
providers/OpenAIResponses/GetResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/DeleteResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/CancelResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/ListInputItemsRequest.hpp
|
||||||
|
providers/OpenAIResponses/InputTokensRequest.hpp
|
||||||
|
providers/OpenAIResponses/ItemTypesReference.hpp
|
||||||
|
providers/OpenAIResponsesRequestBuilder.hpp
|
||||||
|
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
||||||
|
providers/OpenAIResponsesMessage.hpp providers/OpenAIResponsesMessage.cpp
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
@@ -121,10 +135,13 @@ add_qtc_plugin(QodeAssist
|
|||||||
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
|
||||||
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
|
||||||
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
|
||||||
|
widgets/RefactorWidget.hpp widgets/RefactorWidget.cpp
|
||||||
|
widgets/RefactorWidgetHandler.hpp widgets/RefactorWidgetHandler.cpp
|
||||||
|
widgets/ContextExtractor.hpp
|
||||||
|
widgets/DiffStatistics.hpp
|
||||||
|
|
||||||
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
||||||
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
||||||
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
|
|
||||||
tools/ToolHandler.hpp tools/ToolHandler.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/ToolsManager.hpp tools/ToolsManager.cpp
|
||||||
@@ -132,9 +149,11 @@ add_qtc_plugin(QodeAssist
|
|||||||
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
||||||
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
||||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
||||||
|
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
|
||||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
||||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||||
|
tools/TodoTool.hpp tools/TodoTool.cpp
|
||||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
||||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
||||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
|
|
||||||
qml/controls/AttachedFilesPlace.qml
|
qml/controls/AttachedFilesPlace.qml
|
||||||
qml/controls/BottomBar.qml
|
qml/controls/BottomBar.qml
|
||||||
|
qml/controls/FileMentionPopup.qml
|
||||||
qml/controls/FileEditsActionBar.qml
|
qml/controls/FileEditsActionBar.qml
|
||||||
qml/controls/RulesViewer.qml
|
qml/controls/ContextViewer.qml
|
||||||
qml/controls/Toast.qml
|
qml/controls/Toast.qml
|
||||||
qml/controls/TopBar.qml
|
qml/controls/TopBar.qml
|
||||||
qml/controls/SplitDropZone.qml
|
qml/controls/SplitDropZone.qml
|
||||||
@@ -43,12 +44,17 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/chat-icon.svg
|
icons/chat-icon.svg
|
||||||
icons/chat-pause-icon.svg
|
icons/chat-pause-icon.svg
|
||||||
icons/rules-icon.svg
|
icons/rules-icon.svg
|
||||||
|
icons/context-icon.svg
|
||||||
icons/open-in-editor.svg
|
icons/open-in-editor.svg
|
||||||
icons/apply-changes-button.svg
|
icons/apply-changes-button.svg
|
||||||
icons/undo-changes-button.svg
|
icons/undo-changes-button.svg
|
||||||
icons/reject-changes-button.svg
|
icons/reject-changes-button.svg
|
||||||
icons/thinking-icon-on.svg
|
icons/thinking-icon-on.svg
|
||||||
icons/thinking-icon-off.svg
|
icons/thinking-icon-off.svg
|
||||||
|
icons/tools-icon-on.svg
|
||||||
|
icons/tools-icon-off.svg
|
||||||
|
icons/settings-icon.svg
|
||||||
|
icons/compress-icon.svg
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
@@ -61,6 +67,9 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
ChatView.hpp ChatView.cpp
|
ChatView.hpp ChatView.cpp
|
||||||
ChatData.hpp
|
ChatData.hpp
|
||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
|
FileMentionItem.hpp FileMentionItem.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
|
|||||||
310
ChatView/ChatCompressor.cpp
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ChatCompressor.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 = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (!m_provider) {
|
||||||
|
emit compressionFailed(tr("No provider available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto templateName = Settings::generalSettings().caTemplate();
|
||||||
|
auto promptTemplate = LLMCore::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();
|
||||||
|
m_currentRequestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
|
||||||
|
emit compressionStarted();
|
||||||
|
|
||||||
|
connectProviderSignals();
|
||||||
|
|
||||||
|
QUrl requestUrl;
|
||||||
|
QJsonObject payload;
|
||||||
|
|
||||||
|
if (m_provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||||
|
requestUrl = QUrl(QString("%1/models/%2:streamGenerateContent?alt=sse")
|
||||||
|
.arg(Settings::generalSettings().caUrl(),
|
||||||
|
Settings::generalSettings().caModel()));
|
||||||
|
} else {
|
||||||
|
requestUrl = QUrl(QString("%1%2").arg(Settings::generalSettings().caUrl(),
|
||||||
|
m_provider->chatEndpoint()));
|
||||||
|
payload["model"] = Settings::generalSettings().caModel();
|
||||||
|
payload["stream"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRequestPayload(payload, promptTemplate);
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
|
m_provider->sendRequest(m_currentRequestId, requestUrl, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, LLMCore::PromptTemplate *promptTemplate)
|
||||||
|
{
|
||||||
|
LLMCore::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<LLMCore::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;
|
||||||
|
|
||||||
|
LLMCore::Message apiMessage;
|
||||||
|
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||||
|
apiMessage.content = msg.content;
|
||||||
|
messages.append(apiMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
LLMCore::Message compressionRequest;
|
||||||
|
compressionRequest.role = "user";
|
||||||
|
compressionRequest.content = buildCompressionPrompt();
|
||||||
|
messages.append(compressionRequest);
|
||||||
|
|
||||||
|
context.history = messages;
|
||||||
|
|
||||||
|
m_provider->prepareRequest(
|
||||||
|
payload, promptTemplate, context, LLMCore::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()
|
||||||
|
{
|
||||||
|
m_connections.append(connect(
|
||||||
|
m_provider,
|
||||||
|
&LLMCore::Provider::partialResponseReceived,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onPartialResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
m_provider,
|
||||||
|
&LLMCore::Provider::fullResponseReceived,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onFullResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
m_provider,
|
||||||
|
&LLMCore::Provider::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
|
||||||
79
ChatView/ChatCompressor.hpp
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::LLMCore {
|
||||||
|
class Provider;
|
||||||
|
class PromptTemplate;
|
||||||
|
} // namespace QodeAssist::LLMCore
|
||||||
|
|
||||||
|
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, LLMCore::PromptTemplate *promptTemplate);
|
||||||
|
|
||||||
|
bool m_isCompressing = false;
|
||||||
|
QString m_currentRequestId;
|
||||||
|
QString m_originalChatPath;
|
||||||
|
QString m_accumulatedSummary;
|
||||||
|
LLMCore::Provider *m_provider = nullptr;
|
||||||
|
ChatModel *m_chatModel = nullptr;
|
||||||
|
|
||||||
|
QList<QMetaObject::Connection> m_connections;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
206
ChatView/ChatFileManager.cpp
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ChatFileManager.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatFileManager::ChatFileManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_intermediateStorageDir(getIntermediateStorageDir())
|
||||||
|
{}
|
||||||
|
|
||||||
|
ChatFileManager::~ChatFileManager() = default;
|
||||||
|
|
||||||
|
QStringList ChatFileManager::processDroppedFiles(const QStringList &filePaths)
|
||||||
|
{
|
||||||
|
QStringList processedPaths;
|
||||||
|
processedPaths.reserve(filePaths.size());
|
||||||
|
|
||||||
|
for (const QString &filePath : filePaths) {
|
||||||
|
if (!isFileAccessible(filePath)) {
|
||||||
|
const QString error = tr("File is not accessible: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString copiedPath = copyToIntermediateStorage(filePath);
|
||||||
|
if (!copiedPath.isEmpty()) {
|
||||||
|
processedPaths.append(copiedPath);
|
||||||
|
emit fileCopiedToStorage(filePath, copiedPath);
|
||||||
|
LOG_MESSAGE(QString("File copied to storage: %1 -> %2").arg(filePath, copiedPath));
|
||||||
|
} else {
|
||||||
|
const QString error = tr("Failed to copy file: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::setChatFilePath(const QString &chatFilePath)
|
||||||
|
{
|
||||||
|
m_chatFilePath = chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::chatFilePath() const
|
||||||
|
{
|
||||||
|
return m_chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::clearIntermediateStorage()
|
||||||
|
{
|
||||||
|
QDir dir(m_intermediateStorageDir);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
LOG_MESSAGE(QString("Removed intermediate file: %1").arg(fileInfo.fileName()));
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("Failed to remove intermediate file: %1")
|
||||||
|
.arg(fileInfo.fileName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatFileManager::isFileAccessible(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
return fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::cleanupGlobalIntermediateStorage()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir(intermediatePath);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
int removedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
removedCount++;
|
||||||
|
} else {
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0 || failedCount > 0) {
|
||||||
|
LOG_MESSAGE(QString("ChatFileManager global cleanup: removed=%1, failed=%2")
|
||||||
|
.arg(removedCount)
|
||||||
|
.arg(failedCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::copyToIntermediateStorage(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
if (!fileInfo.exists() || !fileInfo.isFile()) {
|
||||||
|
LOG_MESSAGE(QString("Source file does not exist or is not a file: %1").arg(filePath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo.size() == 0) {
|
||||||
|
LOG_MESSAGE(QString("Source file is empty: %1").arg(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString newFileName = generateIntermediateFileName(filePath);
|
||||||
|
const QString destinationPath = QDir(m_intermediateStorageDir).filePath(newFileName);
|
||||||
|
|
||||||
|
if (QFileInfo::exists(destinationPath)) {
|
||||||
|
QFile::remove(destinationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QFile::copy(filePath, destinationPath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to copy file: %1 -> %2").arg(filePath, destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile copiedFile(destinationPath);
|
||||||
|
if (!copiedFile.exists()) {
|
||||||
|
LOG_MESSAGE(QString("Copied file does not exist after copy: %1").arg(destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
copiedFile.setPermissions(QFile::ReadUser | QFile::WriteUser);
|
||||||
|
|
||||||
|
return destinationPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::getIntermediateStorageDir()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir;
|
||||||
|
if (!dir.exists(intermediatePath) && !dir.mkpath(intermediatePath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create intermediate storage directory: %1")
|
||||||
|
.arg(intermediatePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return intermediatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::generateIntermediateFileName(const QString &originalPath)
|
||||||
|
{
|
||||||
|
const QFileInfo fileInfo(originalPath);
|
||||||
|
const QString extension = fileInfo.suffix();
|
||||||
|
QString baseName = fileInfo.completeBaseName().left(30);
|
||||||
|
|
||||||
|
static const QRegularExpression specialChars("[^a-zA-Z0-9_-]");
|
||||||
|
baseName.replace(specialChars, "_");
|
||||||
|
|
||||||
|
if (baseName.isEmpty()) {
|
||||||
|
baseName = "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
|
||||||
|
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces).left(8);
|
||||||
|
|
||||||
|
return QString("%1_%2_%3.%4").arg(baseName, timestamp, uuid, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
59
ChatView/ChatFileManager.hpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QMap>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatFileManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatFileManager(QObject *parent = nullptr);
|
||||||
|
~ChatFileManager();
|
||||||
|
|
||||||
|
QStringList processDroppedFiles(const QStringList &filePaths);
|
||||||
|
void setChatFilePath(const QString &chatFilePath);
|
||||||
|
QString chatFilePath() const;
|
||||||
|
void clearIntermediateStorage();
|
||||||
|
|
||||||
|
static bool isFileAccessible(const QString &filePath);
|
||||||
|
static void cleanupGlobalIntermediateStorage();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileOperationFailed(const QString &error);
|
||||||
|
void fileCopiedToStorage(const QString &originalPath, const QString &newPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString copyToIntermediateStorage(const QString &filePath);
|
||||||
|
QString getIntermediateStorageDir();
|
||||||
|
QString generateIntermediateFileName(const QString &originalPath);
|
||||||
|
|
||||||
|
QString m_chatFilePath;
|
||||||
|
QString m_intermediateStorageDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
@@ -78,11 +78,26 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
return message.content;
|
return message.content;
|
||||||
}
|
}
|
||||||
case Roles::Attachments: {
|
case Roles::Attachments: {
|
||||||
QStringList filenames;
|
QVariantList attachmentsList;
|
||||||
for (const auto &attachment : message.attachments) {
|
for (const auto &attachment : message.attachments) {
|
||||||
filenames << attachment.filename;
|
QVariantMap attachmentMap;
|
||||||
|
attachmentMap["fileName"] = attachment.filename;
|
||||||
|
attachmentMap["storedPath"] = attachment.content;
|
||||||
|
|
||||||
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
|
QFileInfo fileInfo(m_chatFilePath);
|
||||||
|
QString baseName = fileInfo.completeBaseName();
|
||||||
|
QString dirPath = fileInfo.absolutePath();
|
||||||
|
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||||
|
QString fullPath = QDir(contentFolder).filePath(attachment.content);
|
||||||
|
attachmentMap["filePath"] = fullPath;
|
||||||
|
} else {
|
||||||
|
attachmentMap["filePath"] = QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsList.append(attachmentMap);
|
||||||
}
|
}
|
||||||
return filenames;
|
return attachmentsList;
|
||||||
}
|
}
|
||||||
case Roles::IsRedacted: {
|
case Roles::IsRedacted: {
|
||||||
return message.isRedacted;
|
return message.isRedacted;
|
||||||
@@ -95,16 +110,17 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
imageMap["storedPath"] = image.storedPath;
|
imageMap["storedPath"] = image.storedPath;
|
||||||
imageMap["mediaType"] = image.mediaType;
|
imageMap["mediaType"] = image.mediaType;
|
||||||
|
|
||||||
// Generate proper file URL for cross-platform compatibility
|
|
||||||
if (!m_chatFilePath.isEmpty()) {
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
QFileInfo fileInfo(m_chatFilePath);
|
QFileInfo fileInfo(m_chatFilePath);
|
||||||
QString baseName = fileInfo.completeBaseName();
|
QString baseName = fileInfo.completeBaseName();
|
||||||
QString dirPath = fileInfo.absolutePath();
|
QString dirPath = fileInfo.absolutePath();
|
||||||
QString imagesFolder = QDir(dirPath).filePath(baseName + "_images");
|
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||||
QString fullPath = QDir(imagesFolder).filePath(image.storedPath);
|
QString fullPath = QDir(contentFolder).filePath(image.storedPath);
|
||||||
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
||||||
|
imageMap["filePath"] = fullPath;
|
||||||
} else {
|
} else {
|
||||||
imageMap["imageUrl"] = QString();
|
imageMap["imageUrl"] = QString();
|
||||||
|
imageMap["filePath"] = QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
imagesList.append(imageMap);
|
imagesList.append(imageMap);
|
||||||
@@ -132,29 +148,26 @@ void ChatModel::addMessage(
|
|||||||
ChatRole role,
|
ChatRole role,
|
||||||
const QString &id,
|
const QString &id,
|
||||||
const QList<Context::ContentFile> &attachments,
|
const QList<Context::ContentFile> &attachments,
|
||||||
const QList<ImageAttachment> &images)
|
const QList<ImageAttachment> &images,
|
||||||
|
bool isRedacted,
|
||||||
|
const QString &signature)
|
||||||
{
|
{
|
||||||
QString fullContent = content;
|
|
||||||
if (!attachments.isEmpty()) {
|
|
||||||
fullContent += "\n\nAttached files list:";
|
|
||||||
for (const auto &attachment : attachments) {
|
|
||||||
fullContent += QString("\nname: %1\nfile content:\n%2")
|
|
||||||
.arg(attachment.filename, attachment.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
||||||
&& m_messages.last().role == role) {
|
&& m_messages.last().role == role) {
|
||||||
Message &lastMessage = m_messages.last();
|
Message &lastMessage = m_messages.last();
|
||||||
lastMessage.content = content;
|
lastMessage.content = content;
|
||||||
lastMessage.attachments = attachments;
|
lastMessage.attachments = attachments;
|
||||||
lastMessage.images = images;
|
lastMessage.images = images;
|
||||||
|
lastMessage.isRedacted = isRedacted;
|
||||||
|
lastMessage.signature = signature;
|
||||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||||
} else {
|
} else {
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||||
Message newMessage{role, content, id};
|
Message newMessage{role, content, id};
|
||||||
newMessage.attachments = attachments;
|
newMessage.attachments = attachments;
|
||||||
newMessage.images = images;
|
newMessage.images = images;
|
||||||
|
newMessage.isRedacted = isRedacted;
|
||||||
|
newMessage.signature = signature;
|
||||||
m_messages.append(newMessage);
|
m_messages.append(newMessage);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|
||||||
@@ -450,6 +463,16 @@ void ChatModel::addThinkingBlock(
|
|||||||
displayContent += "\n[Signature: " + signature.left(40) + "...]";
|
displayContent += "\n[Signature: " + signature.left(40) + "...]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_messages.size(); ++i) {
|
||||||
|
if (m_messages[i].role == ChatRole::Thinking && m_messages[i].id == requestId) {
|
||||||
|
m_messages[i].content = displayContent;
|
||||||
|
m_messages[i].signature = signature;
|
||||||
|
emit dataChanged(index(i), index(i));
|
||||||
|
LOG_MESSAGE(QString("Updated existing thinking message at index %1").arg(i));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||||
Message thinkingMessage;
|
Message thinkingMessage;
|
||||||
thinkingMessage.role = ChatRole::Thinking;
|
thinkingMessage.role = ChatRole::Thinking;
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ public:
|
|||||||
ChatRole role,
|
ChatRole role,
|
||||||
const QString &id,
|
const QString &id,
|
||||||
const QList<Context::ContentFile> &attachments = {},
|
const QList<Context::ContentFile> &attachments = {},
|
||||||
const QList<ImageAttachment> &images = {});
|
const QList<ImageAttachment> &images = {},
|
||||||
|
bool isRedacted = false,
|
||||||
|
const QString &signature = QString());
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -21,8 +21,12 @@
|
|||||||
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
|
#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>
|
||||||
@@ -33,18 +37,21 @@
|
|||||||
#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"
|
||||||
#include "ToolsSettings.hpp"
|
#include "SettingsConstants.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
|
#include "ProvidersManager.hpp"
|
||||||
|
#include "ToolsSettings.hpp"
|
||||||
#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 "llmcore/RulesLoader.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -53,7 +60,9 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
, m_chatModel(new ChatModel(this))
|
, m_chatModel(new ChatModel(this))
|
||||||
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
|
, m_promptProvider(LLMCore::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_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
|
, m_chatCompressor(new ChatCompressor(this))
|
||||||
{
|
{
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(
|
||||||
@@ -69,15 +78,15 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
|
|
||||||
connect(&settings.caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
connect(&settings.caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
||||||
auto &settings = Settings::generalSettings();
|
auto &settings = Settings::generalSettings();
|
||||||
m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(),
|
m_currentConfiguration
|
||||||
settings.caModel.value());
|
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
||||||
emit currentConfigurationChanged();
|
emit currentConfigurationChanged();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(&settings.caModel, &Utils::BaseAspect::changed, this, [this]() {
|
connect(&settings.caModel, &Utils::BaseAspect::changed, this, [this]() {
|
||||||
auto &settings = Settings::generalSettings();
|
auto &settings = Settings::generalSettings();
|
||||||
m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(),
|
m_currentConfiguration
|
||||||
settings.caModel.value());
|
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
||||||
emit currentConfigurationChanged();
|
emit currentConfigurationChanged();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,6 +123,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();
|
||||||
|
|
||||||
@@ -164,7 +178,8 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
|
|
||||||
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
|
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
|
||||||
if (!m_currentMessageRequestId.isEmpty()) {
|
if (!m_currentMessageRequestId.isEmpty()) {
|
||||||
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId));
|
LOG_MESSAGE(
|
||||||
|
QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentMessageRequestId = requestId;
|
m_currentMessageRequestId = requestId;
|
||||||
@@ -205,6 +220,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
refreshRules();
|
refreshRules();
|
||||||
loadAvailableConfigurations();
|
loadAvailableConfigurations();
|
||||||
|
loadAvailableAgentRoles();
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
@@ -212,26 +228,61 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::refreshRules);
|
&ChatRootView::refreshRules);
|
||||||
|
|
||||||
QSettings appSettings;
|
connect(
|
||||||
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
m_isThinkingMode = Settings::chatAssistantSettings().enableThinkingMode();
|
&ProjectExplorer::ProjectManager::projectAdded,
|
||||||
|
this,
|
||||||
|
&ChatRootView::openFilesChanged);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
|
&ProjectExplorer::ProjectManager::projectRemoved,
|
||||||
|
this,
|
||||||
|
&ChatRootView::openFilesChanged);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
&Settings::chatAssistantSettings().enableChatTools,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatRootView::useToolsChanged);
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&Settings::chatAssistantSettings().enableThinkingMode,
|
&Settings::chatAssistantSettings().enableThinkingMode,
|
||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
this,
|
this,
|
||||||
[this]() { setIsThinkingMode(Settings::chatAssistantSettings().enableThinkingMode()); });
|
&ChatRootView::useThinkingChanged);
|
||||||
|
|
||||||
connect(
|
|
||||||
&Settings::toolsSettings().useTools,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::toolsSupportEnabledChanged);
|
|
||||||
connect(
|
connect(
|
||||||
&Settings::generalSettings().caProvider,
|
&Settings::generalSettings().caProvider,
|
||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::isThinkingSupportChanged);
|
&ChatRootView::isThinkingSupportChanged);
|
||||||
|
|
||||||
|
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
||||||
|
m_lastErrorMessage = error;
|
||||||
|
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
|
||||||
@@ -265,7 +316,10 @@ void ChatRootView::sendMessage(const QString &message)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles, m_isAgentMode);
|
m_clientInterface
|
||||||
|
->sendMessage(message, m_attachmentFiles, m_linkedFiles, useTools(), useThinking());
|
||||||
|
|
||||||
|
m_fileManager->clearIntermediateStorage();
|
||||||
clearAttachmentFiles();
|
clearAttachmentFiles();
|
||||||
setRequestProgressStatus(true);
|
setRequestProgressStatus(true);
|
||||||
}
|
}
|
||||||
@@ -283,18 +337,29 @@ void ChatRootView::cancelRequest()
|
|||||||
|
|
||||||
void ChatRootView::clearAttachmentFiles()
|
void ChatRootView::clearAttachmentFiles()
|
||||||
{
|
{
|
||||||
if (!m_attachmentFiles.isEmpty()) {
|
if (m_attachmentFiles.isEmpty()) {
|
||||||
m_attachmentFiles.clear();
|
return;
|
||||||
emit attachmentFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_attachmentFiles.clear();
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
m_fileManager->clearIntermediateStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::clearLinkedFiles()
|
void ChatRootView::clearLinkedFiles()
|
||||||
{
|
{
|
||||||
if (!m_linkedFiles.isEmpty()) {
|
if (m_linkedFiles.isEmpty()) {
|
||||||
m_linkedFiles.clear();
|
return;
|
||||||
emit linkedFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_linkedFiles.clear();
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::clearMessages()
|
||||||
|
{
|
||||||
|
m_clientInterface->clearMessages();
|
||||||
|
clearLinkedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::getChatsHistoryDir() const
|
QString ChatRootView::getChatsHistoryDir() const
|
||||||
@@ -305,8 +370,8 @@ QString ChatRootView::getChatsHistoryDir() const
|
|||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
} else {
|
} else {
|
||||||
path = QString("%1/qodeassist/chat_history")
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir dir(path);
|
QDir dir(path);
|
||||||
@@ -343,6 +408,12 @@ void ChatRootView::loadHistory(const QString &filePath)
|
|||||||
setRecentFilePath(filePath);
|
setRecentFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_fileManager->clearIntermediateStorage();
|
||||||
|
m_attachmentFiles.clear();
|
||||||
|
m_linkedFiles.clear();
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
|
||||||
m_currentMessageRequestId.clear();
|
m_currentMessageRequestId.clear();
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
updateCurrentMessageEditsStats();
|
updateCurrentMessageEditsStats();
|
||||||
@@ -410,7 +481,8 @@ QString ChatRootView::getSuggestedFileName() const
|
|||||||
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||||
|
|
||||||
if (shortMessage.isEmpty()) {
|
if (shortMessage.isEmpty()) {
|
||||||
QVariantList images = m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
QVariantList images
|
||||||
|
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
||||||
if (!images.isEmpty()) {
|
if (!images.isEmpty()) {
|
||||||
shortMessage = "image_chat";
|
shortMessage = "image_chat";
|
||||||
}
|
}
|
||||||
@@ -447,7 +519,8 @@ QString ChatRootView::getAutosaveFilePath() const
|
|||||||
return QDir(dir).filePath(getSuggestedFileName() + ".json");
|
return QDir(dir).filePath(getSuggestedFileName() + ".json");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::getAutosaveFilePath(const QString &firstMessage, const QStringList &attachments) const
|
QString ChatRootView::getAutosaveFilePath(
|
||||||
|
const QString &firstMessage, const QStringList &attachments) const
|
||||||
{
|
{
|
||||||
if (!m_recentFilePath.isEmpty()) {
|
if (!m_recentFilePath.isEmpty()) {
|
||||||
return m_recentFilePath;
|
return m_recentFilePath;
|
||||||
@@ -498,8 +571,10 @@ void ChatRootView::addFilesToAttachList(const QStringList &filePaths)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QStringList processedPaths = m_fileManager->processDroppedFiles(filePaths);
|
||||||
|
|
||||||
bool filesAdded = false;
|
bool filesAdded = false;
|
||||||
for (const QString &filePath : filePaths) {
|
for (const QString &filePath : processedPaths) {
|
||||||
if (!m_attachmentFiles.contains(filePath)) {
|
if (!m_attachmentFiles.contains(filePath)) {
|
||||||
m_attachmentFiles.append(filePath);
|
m_attachmentFiles.append(filePath);
|
||||||
filesAdded = true;
|
filesAdded = true;
|
||||||
@@ -513,10 +588,15 @@ void ChatRootView::addFilesToAttachList(const QStringList &filePaths)
|
|||||||
|
|
||||||
void ChatRootView::removeFileFromAttachList(int index)
|
void ChatRootView::removeFileFromAttachList(int index)
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < m_attachmentFiles.size()) {
|
if (index < 0 || index >= m_attachmentFiles.size()) {
|
||||||
m_attachmentFiles.removeAt(index);
|
return;
|
||||||
emit attachmentFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString removedFile = m_attachmentFiles.at(index);
|
||||||
|
m_attachmentFiles.removeAt(index);
|
||||||
|
emit attachmentFilesChanged();
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Removed attachment file: %1").arg(removedFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showLinkFilesDialog()
|
void ChatRootView::showLinkFilesDialog()
|
||||||
@@ -556,8 +636,8 @@ void ChatRootView::addFilesToLinkList(const QStringList &filePaths)
|
|||||||
|
|
||||||
if (!imageFiles.isEmpty()) {
|
if (!imageFiles.isEmpty()) {
|
||||||
addFilesToAttachList(imageFiles);
|
addFilesToAttachList(imageFiles);
|
||||||
|
m_lastInfoMessage
|
||||||
m_lastInfoMessage = tr("Images automatically moved to Attach zone (%n file(s))", "", imageFiles.size());
|
= tr("Images automatically moved to Attach zone (%n file(s))", "", imageFiles.size());
|
||||||
emit lastInfoMessageChanged();
|
emit lastInfoMessageChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,10 +648,15 @@ void ChatRootView::addFilesToLinkList(const QStringList &filePaths)
|
|||||||
|
|
||||||
void ChatRootView::removeFileFromLinkList(int index)
|
void ChatRootView::removeFileFromLinkList(int index)
|
||||||
{
|
{
|
||||||
if (index >= 0 && index < m_linkedFiles.size()) {
|
if (index < 0 || index >= m_linkedFiles.size()) {
|
||||||
m_linkedFiles.removeAt(index);
|
return;
|
||||||
emit linkedFilesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString removedFile = m_linkedFiles.at(index);
|
||||||
|
m_linkedFiles.removeAt(index);
|
||||||
|
emit linkedFilesChanged();
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Removed linked file: %1").arg(removedFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showAddImageDialog()
|
void ChatRootView::showAddImageDialog()
|
||||||
@@ -585,19 +670,7 @@ void ChatRootView::showAddImageDialog()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dialog.exec() == QDialog::Accepted) {
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
QStringList newFilePaths = dialog.selectedFiles();
|
addFilesToAttachList(dialog.selectedFiles());
|
||||||
if (!newFilePaths.isEmpty()) {
|
|
||||||
bool filesAdded = false;
|
|
||||||
for (const QString &filePath : std::as_const(newFilePaths)) {
|
|
||||||
if (!m_attachmentFiles.contains(filePath)) {
|
|
||||||
m_attachmentFiles.append(filePath);
|
|
||||||
filesAdded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filesAdded) {
|
|
||||||
emit attachmentFilesChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,8 +716,8 @@ void ChatRootView::openChatHistoryFolder()
|
|||||||
Settings::ProjectSettings projectSettings(project);
|
Settings::ProjectSettings projectSettings(project);
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
} else {
|
} else {
|
||||||
path = QString("%1/qodeassist/chat_history")
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir dir(path);
|
QDir dir(path);
|
||||||
@@ -664,7 +737,7 @@ void ChatRootView::openRulesFolder()
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString projectPath = project->projectDirectory().toFSPathString();
|
QString projectPath = project->projectDirectory().toFSPathString();
|
||||||
QString rulesPath = projectPath + "/.qodeassist/rules";
|
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
||||||
|
|
||||||
QDir dir(rulesPath);
|
QDir dir(rulesPath);
|
||||||
if (!dir.exists()) {
|
if (!dir.exists()) {
|
||||||
@@ -675,6 +748,18 @@ void ChatRootView::openRulesFolder()
|
|||||||
QDesktopServices::openUrl(url);
|
QDesktopServices::openUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatRootView::openSettings()
|
||||||
|
{
|
||||||
|
Settings::showSettings(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::openFileInEditor(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
void ChatRootView::updateInputTokensCount()
|
void ChatRootView::updateInputTokensCount()
|
||||||
{
|
{
|
||||||
int inputTokens = m_messageTokensCount;
|
int inputTokens = m_messageTokensCount;
|
||||||
@@ -725,6 +810,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)
|
||||||
@@ -742,6 +829,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,6 +848,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
|
|||||||
if (m_recentFilePath != filePath) {
|
if (m_recentFilePath != filePath) {
|
||||||
m_recentFilePath = filePath;
|
m_recentFilePath = filePath;
|
||||||
m_clientInterface->setChatFilePath(filePath);
|
m_clientInterface->setChatFilePath(filePath);
|
||||||
|
m_fileManager->setChatFilePath(filePath);
|
||||||
emit chatFileNameChanged();
|
emit chatFileNameChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -867,43 +956,26 @@ void ChatRootView::refreshRules()
|
|||||||
emit activeRulesCountChanged();
|
emit activeRulesCountChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::isAgentMode() const
|
bool ChatRootView::useTools() const
|
||||||
{
|
{
|
||||||
return m_isAgentMode;
|
return Settings::chatAssistantSettings().enableChatTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::setIsAgentMode(bool newIsAgentMode)
|
void ChatRootView::setUseTools(bool enabled)
|
||||||
{
|
{
|
||||||
if (m_isAgentMode != newIsAgentMode) {
|
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
|
||||||
m_isAgentMode = newIsAgentMode;
|
Settings::chatAssistantSettings().writeSettings();
|
||||||
|
|
||||||
QSettings settings;
|
|
||||||
settings.setValue("QodeAssist/Chat/AgentMode", newIsAgentMode);
|
|
||||||
|
|
||||||
emit isAgentModeChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::isThinkingMode() const
|
bool ChatRootView::useThinking() const
|
||||||
{
|
{
|
||||||
return m_isThinkingMode;
|
return Settings::chatAssistantSettings().enableThinkingMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::setIsThinkingMode(bool newIsThinkingMode)
|
void ChatRootView::setUseThinking(bool enabled)
|
||||||
{
|
{
|
||||||
if (m_isThinkingMode != newIsThinkingMode) {
|
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
|
||||||
m_isThinkingMode = newIsThinkingMode;
|
Settings::chatAssistantSettings().writeSettings();
|
||||||
|
|
||||||
Settings::chatAssistantSettings().enableThinkingMode.setValue(newIsThinkingMode);
|
|
||||||
Settings::chatAssistantSettings().writeSettings();
|
|
||||||
|
|
||||||
emit isThinkingModeChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::toolsSupportEnabled() const
|
|
||||||
{
|
|
||||||
return Settings::toolsSettings().useTools();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::applyFileEdit(const QString &editId)
|
void ChatRootView::applyFileEdit(const QString &editId)
|
||||||
@@ -917,8 +989,8 @@ void ChatRootView::applyFileEdit(const QString &editId)
|
|||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
||||||
? QString("Failed to apply file edit")
|
? QString("Failed to apply file edit")
|
||||||
: QString("Failed to apply file edit: %1").arg(edit.statusMessage);
|
: QString("Failed to apply file edit: %1").arg(edit.statusMessage);
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -934,8 +1006,8 @@ void ChatRootView::rejectFileEdit(const QString &editId)
|
|||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
||||||
? QString("Failed to reject file edit")
|
? QString("Failed to reject file edit")
|
||||||
: QString("Failed to reject file edit: %1").arg(edit.statusMessage);
|
: QString("Failed to reject file edit: %1").arg(edit.statusMessage);
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -951,8 +1023,8 @@ void ChatRootView::undoFileEdit(const QString &editId)
|
|||||||
} else {
|
} else {
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
||||||
? QString("Failed to undo file edit")
|
? QString("Failed to undo file edit")
|
||||||
: QString("Failed to undo file edit: %1").arg(edit.statusMessage);
|
: QString("Failed to undo file edit: %1").arg(edit.statusMessage);
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -986,8 +1058,7 @@ void ChatRootView::openFileEditInEditor(const QString &editId)
|
|||||||
|
|
||||||
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
||||||
position = currentContent.indexOf(edit.newContent);
|
position = currentContent.indexOf(edit.newContent);
|
||||||
}
|
} else if (!edit.oldContent.isEmpty()) {
|
||||||
else if (!edit.oldContent.isEmpty()) {
|
|
||||||
position = currentContent.indexOf(edit.oldContent);
|
position = currentContent.indexOf(edit.oldContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,7 +1099,9 @@ void ChatRootView::updateFileEditStatus(const QString &editId, const QString &st
|
|||||||
obj["status_message"] = edit.statusMessage;
|
obj["status_message"] = edit.statusMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString updatedContent = marker + QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
QString updatedContent = marker
|
||||||
|
+ QString::fromUtf8(
|
||||||
|
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
m_chatModel->updateMessageContent(editId, updatedContent);
|
||||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
||||||
}
|
}
|
||||||
@@ -1057,7 +1130,8 @@ void ChatRootView::applyAllFileEditsForCurrentMessage()
|
|||||||
m_lastInfoMessage = QString("All file edits applied successfully");
|
m_lastInfoMessage = QString("All file edits applied successfully");
|
||||||
emit lastInfoMessageChanged();
|
emit lastInfoMessageChanged();
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
||||||
|
m_currentMessageRequestId);
|
||||||
for (const auto &edit : edits) {
|
for (const auto &edit : edits) {
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
if (edit.status == Context::ChangesManager::Applied) {
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
updateFileEditStatus(edit.editId, "applied");
|
||||||
@@ -1065,11 +1139,12 @@ void ChatRootView::applyAllFileEditsForCurrentMessage()
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_lastErrorMessage = errorMsg.isEmpty()
|
m_lastErrorMessage = errorMsg.isEmpty()
|
||||||
? QString("Failed to apply some file edits")
|
? QString("Failed to apply some file edits")
|
||||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg);
|
: QString("Failed to apply some file edits:\n%1").arg(errorMsg);
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
||||||
|
m_currentMessageRequestId);
|
||||||
for (const auto &edit : edits) {
|
for (const auto &edit : edits) {
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
if (edit.status == Context::ChangesManager::Applied) {
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
updateFileEditStatus(edit.editId, "applied");
|
||||||
@@ -1098,7 +1173,8 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
|
|||||||
m_lastInfoMessage = QString("All file edits undone successfully");
|
m_lastInfoMessage = QString("All file edits undone successfully");
|
||||||
emit lastInfoMessageChanged();
|
emit lastInfoMessageChanged();
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
||||||
|
m_currentMessageRequestId);
|
||||||
for (const auto &edit : edits) {
|
for (const auto &edit : edits) {
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
if (edit.status == Context::ChangesManager::Rejected) {
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
updateFileEditStatus(edit.editId, "rejected");
|
||||||
@@ -1106,11 +1182,12 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m_lastErrorMessage = errorMsg.isEmpty()
|
m_lastErrorMessage = errorMsg.isEmpty()
|
||||||
? QString("Failed to undo some file edits")
|
? QString("Failed to undo some file edits")
|
||||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg);
|
: QString("Failed to undo some file edits:\n%1").arg(errorMsg);
|
||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
||||||
|
m_currentMessageRequestId);
|
||||||
for (const auto &edit : edits) {
|
for (const auto &edit : edits) {
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
if (edit.status == Context::ChangesManager::Rejected) {
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
updateFileEditStatus(edit.editId, "rejected");
|
||||||
@@ -1124,8 +1201,8 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
|
|||||||
void ChatRootView::updateCurrentMessageEditsStats()
|
void ChatRootView::updateCurrentMessageEditsStats()
|
||||||
{
|
{
|
||||||
if (m_currentMessageRequestId.isEmpty()) {
|
if (m_currentMessageRequestId.isEmpty()) {
|
||||||
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 ||
|
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0
|
||||||
m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
|
|| m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
|
||||||
m_currentMessageTotalEdits = 0;
|
m_currentMessageTotalEdits = 0;
|
||||||
m_currentMessageAppliedEdits = 0;
|
m_currentMessageAppliedEdits = 0;
|
||||||
m_currentMessagePendingEdits = 0;
|
m_currentMessagePendingEdits = 0;
|
||||||
@@ -1178,8 +1255,12 @@ void ChatRootView::updateCurrentMessageEditsStats()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
LOG_MESSAGE(QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
LOG_MESSAGE(
|
||||||
.arg(total).arg(applied).arg(pending).arg(rejected));
|
QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
||||||
|
.arg(total)
|
||||||
|
.arg(applied)
|
||||||
|
.arg(pending)
|
||||||
|
.arg(rejected));
|
||||||
emit currentMessageEditsStatsChanged();
|
emit currentMessageEditsStatsChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1268,9 +1349,7 @@ bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
|||||||
|
|
||||||
bool ChatRootView::isImageFile(const QString &filePath) const
|
bool ChatRootView::isImageFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
static const QSet<QString> imageExtensions = {
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||||
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"
|
|
||||||
};
|
|
||||||
|
|
||||||
QFileInfo fileInfo(filePath);
|
QFileInfo fileInfo(filePath);
|
||||||
return imageExtensions.contains(fileInfo.suffix().toLower());
|
return imageExtensions.contains(fileInfo.suffix().toLower());
|
||||||
@@ -1318,7 +1397,8 @@ 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.caEndpointMode.setValue(
|
||||||
|
settings.caEndpointMode.indexForDisplay(config.endpointMode));
|
||||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
||||||
|
|
||||||
settings.writeSettings();
|
settings.writeSettings();
|
||||||
@@ -1341,4 +1421,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,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -20,7 +20,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
|
#include "ChatFileManager.hpp"
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
#include "llmcore/PromptProviderChat.hpp"
|
#include "llmcore/PromptProviderChat.hpp"
|
||||||
@@ -28,6 +30,8 @@
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatCompressor;
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
class ChatRootView : public QQuickItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -48,10 +52,8 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
||||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||||
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
|
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
||||||
Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL)
|
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
||||||
Q_PROPERTY(
|
|
||||||
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
|
|
||||||
|
|
||||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
@@ -60,6 +62,12 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
|
||||||
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
||||||
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
||||||
|
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
@@ -95,6 +103,9 @@ public:
|
|||||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
Q_INVOKABLE void openChatHistoryFolder();
|
Q_INVOKABLE void openChatHistoryFolder();
|
||||||
Q_INVOKABLE void openRulesFolder();
|
Q_INVOKABLE void openRulesFolder();
|
||||||
|
Q_INVOKABLE void openSettings();
|
||||||
|
|
||||||
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
|
|
||||||
Q_INVOKABLE void updateInputTokensCount();
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
@@ -127,11 +138,10 @@ public:
|
|||||||
Q_INVOKABLE QString getRuleContent(int index);
|
Q_INVOKABLE QString getRuleContent(int index);
|
||||||
Q_INVOKABLE void refreshRules();
|
Q_INVOKABLE void refreshRules();
|
||||||
|
|
||||||
bool isAgentMode() const;
|
bool useTools() const;
|
||||||
void setIsAgentMode(bool newIsAgentMode);
|
void setUseTools(bool enabled);
|
||||||
bool isThinkingMode() const;
|
bool useThinking() const;
|
||||||
void setIsThinkingMode(bool newIsThinkingMode);
|
void setUseThinking(bool enabled);
|
||||||
bool toolsSupportEnabled() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
||||||
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
||||||
@@ -147,6 +157,18 @@ public:
|
|||||||
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;
|
||||||
int currentMessagePendingEdits() const;
|
int currentMessagePendingEdits() const;
|
||||||
@@ -156,12 +178,15 @@ public:
|
|||||||
|
|
||||||
bool isThinkingSupport() const;
|
bool isThinkingSupport() const;
|
||||||
|
|
||||||
|
bool isCompressing() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
void copyToClipboard(const QString &text);
|
void copyToClipboard(const QString &text);
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
void clearAttachmentFiles();
|
void clearAttachmentFiles();
|
||||||
void clearLinkedFiles();
|
void clearLinkedFiles();
|
||||||
|
void clearMessages();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void chatModelChanged();
|
void chatModelChanged();
|
||||||
@@ -184,15 +209,24 @@ signals:
|
|||||||
void activeRulesChanged();
|
void activeRulesChanged();
|
||||||
void activeRulesCountChanged();
|
void activeRulesCountChanged();
|
||||||
|
|
||||||
void isAgentModeChanged();
|
void useToolsChanged();
|
||||||
void isThinkingModeChanged();
|
void useThinkingChanged();
|
||||||
void toolsSupportEnabledChanged();
|
|
||||||
void currentMessageEditsStatsChanged();
|
void currentMessageEditsStatsChanged();
|
||||||
|
|
||||||
void isThinkingSupportChanged();
|
void isThinkingSupportChanged();
|
||||||
void availableConfigurationsChanged();
|
void availableConfigurationsChanged();
|
||||||
void currentConfigurationChanged();
|
void currentConfigurationChanged();
|
||||||
|
|
||||||
|
void availableAgentRolesChanged();
|
||||||
|
void currentAgentRoleChanged();
|
||||||
|
void baseSystemPromptChanged();
|
||||||
|
|
||||||
|
void isCompressingChanged();
|
||||||
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
void openFilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
@@ -203,6 +237,7 @@ private:
|
|||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
LLMCore::PromptProviderChat m_promptProvider;
|
LLMCore::PromptProviderChat m_promptProvider;
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
|
ChatFileManager *m_fileManager;
|
||||||
QString m_currentTemplate;
|
QString m_currentTemplate;
|
||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
@@ -214,8 +249,6 @@ private:
|
|||||||
bool m_isRequestInProgress;
|
bool m_isRequestInProgress;
|
||||||
QString m_lastErrorMessage;
|
QString m_lastErrorMessage;
|
||||||
QVariantList m_activeRules;
|
QVariantList m_activeRules;
|
||||||
bool m_isAgentMode;
|
|
||||||
bool m_isThinkingMode;
|
|
||||||
|
|
||||||
QString m_currentMessageRequestId;
|
QString m_currentMessageRequestId;
|
||||||
int m_currentMessageTotalEdits{0};
|
int m_currentMessageTotalEdits{0};
|
||||||
@@ -226,6 +259,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
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
const QString ChatSerializer::VERSION = "0.1";
|
const QString ChatSerializer::VERSION = "0.2";
|
||||||
|
|
||||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
||||||
{
|
{
|
||||||
@@ -38,14 +38,6 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {false, "Failed to create directory structure"};
|
return {false, "Failed to create directory structure"};
|
||||||
}
|
}
|
||||||
|
|
||||||
QString imagesFolder = getChatImagesFolder(filePath);
|
|
||||||
QDir dir;
|
|
||||||
if (!dir.exists(imagesFolder)) {
|
|
||||||
if (!dir.mkpath(imagesFolder)) {
|
|
||||||
LOG_MESSAGE(QString("Warning: Failed to create images folder: %1").arg(imagesFolder));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::WriteOnly)) {
|
if (!file.open(QIODevice::WriteOnly)) {
|
||||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||||
@@ -88,17 +80,33 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
|||||||
return {true, QString()};
|
return {true, QString()};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, const QString &chatFilePath)
|
QJsonObject ChatSerializer::serializeMessage(
|
||||||
|
const ChatModel::Message &message, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QJsonObject messageObj;
|
QJsonObject messageObj;
|
||||||
messageObj["role"] = static_cast<int>(message.role);
|
messageObj["role"] = static_cast<int>(message.role);
|
||||||
messageObj["content"] = message.content;
|
messageObj["content"] = message.content;
|
||||||
messageObj["id"] = message.id;
|
messageObj["id"] = message.id;
|
||||||
messageObj["isRedacted"] = message.isRedacted;
|
|
||||||
|
if (message.isRedacted) {
|
||||||
|
messageObj["isRedacted"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!message.signature.isEmpty()) {
|
if (!message.signature.isEmpty()) {
|
||||||
messageObj["signature"] = message.signature;
|
messageObj["signature"] = message.signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!message.attachments.isEmpty()) {
|
||||||
|
QJsonArray attachmentsArray;
|
||||||
|
for (const auto &attachment : message.attachments) {
|
||||||
|
QJsonObject attachmentObj;
|
||||||
|
attachmentObj["fileName"] = attachment.filename;
|
||||||
|
attachmentObj["storedPath"] = attachment.content;
|
||||||
|
attachmentsArray.append(attachmentObj);
|
||||||
|
}
|
||||||
|
messageObj["attachments"] = attachmentsArray;
|
||||||
|
}
|
||||||
|
|
||||||
if (!message.images.isEmpty()) {
|
if (!message.images.isEmpty()) {
|
||||||
QJsonArray imagesArray;
|
QJsonArray imagesArray;
|
||||||
for (const auto &image : message.images) {
|
for (const auto &image : message.images) {
|
||||||
@@ -114,7 +122,8 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
|
|||||||
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());
|
||||||
@@ -123,6 +132,17 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
|||||||
message.isRedacted = json["isRedacted"].toBool(false);
|
message.isRedacted = json["isRedacted"].toBool(false);
|
||||||
message.signature = json["signature"].toString();
|
message.signature = json["signature"].toString();
|
||||||
|
|
||||||
|
if (json.contains("attachments")) {
|
||||||
|
QJsonArray attachmentsArray = json["attachments"].toArray();
|
||||||
|
for (const auto &attachmentValue : attachmentsArray) {
|
||||||
|
QJsonObject attachmentObj = attachmentValue.toObject();
|
||||||
|
Context::ContentFile attachment;
|
||||||
|
attachment.filename = attachmentObj["fileName"].toString();
|
||||||
|
attachment.content = attachmentObj["storedPath"].toString();
|
||||||
|
message.attachments.append(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (json.contains("images")) {
|
if (json.contains("images")) {
|
||||||
QJsonArray imagesArray = json["images"].toArray();
|
QJsonArray imagesArray = json["images"].toArray();
|
||||||
for (const auto &imageValue : imagesArray) {
|
for (const auto &imageValue : imagesArray) {
|
||||||
@@ -152,7 +172,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;
|
||||||
@@ -167,8 +188,18 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
|
|||||||
model->setLoadingFromHistory(true);
|
model->setLoadingFromHistory(true);
|
||||||
|
|
||||||
for (const auto &message : messages) {
|
for (const auto &message : messages) {
|
||||||
model->addMessage(message.content, message.role, message.id, message.attachments, message.images);
|
model->addMessage(
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s)").arg(message.images.size()));
|
message.content,
|
||||||
|
message.role,
|
||||||
|
message.id,
|
||||||
|
message.attachments,
|
||||||
|
message.images,
|
||||||
|
message.isRedacted,
|
||||||
|
message.signature);
|
||||||
|
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
||||||
|
.arg(message.images.size())
|
||||||
|
.arg(message.isRedacted)
|
||||||
|
.arg(message.signature.length()));
|
||||||
}
|
}
|
||||||
|
|
||||||
model->setLoadingFromHistory(false);
|
model->setLoadingFromHistory(false);
|
||||||
@@ -185,27 +216,39 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatSerializer::validateVersion(const QString &version)
|
bool ChatSerializer::validateVersion(const QString &version)
|
||||||
{
|
{
|
||||||
return version == VERSION;
|
if (version == VERSION) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version == "0.1") {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
||||||
|
"to _content");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::getChatImagesFolder(const QString &chatFilePath)
|
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QFileInfo fileInfo(chatFilePath);
|
QFileInfo fileInfo(chatFilePath);
|
||||||
QString baseName = fileInfo.completeBaseName();
|
QString baseName = fileInfo.completeBaseName();
|
||||||
QString dirPath = fileInfo.absolutePath();
|
QString dirPath = fileInfo.absolutePath();
|
||||||
return QDir(dirPath).filePath(baseName + "_images");
|
return QDir(dirPath).filePath(baseName + "_content");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::saveImageToStorage(const QString &chatFilePath,
|
bool ChatSerializer::saveContentToStorage(
|
||||||
const QString &fileName,
|
const QString &chatFilePath,
|
||||||
const QString &base64Data,
|
const QString &fileName,
|
||||||
QString &storedPath)
|
const QString &base64Data,
|
||||||
|
QString &storedPath)
|
||||||
{
|
{
|
||||||
QString imagesFolder = getChatImagesFolder(chatFilePath);
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
QDir dir;
|
QDir dir;
|
||||||
if (!dir.exists(imagesFolder)) {
|
if (!dir.exists(contentFolder)) {
|
||||||
if (!dir.mkpath(imagesFolder)) {
|
if (!dir.mkpath(contentFolder)) {
|
||||||
LOG_MESSAGE(QString("Failed to create images folder: %1").arg(imagesFolder));
|
LOG_MESSAGE(QString("Failed to create content folder: %1").arg(contentFolder));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,47 +257,47 @@ bool ChatSerializer::saveImageToStorage(const QString &chatFilePath,
|
|||||||
QString extension = originalFileInfo.suffix();
|
QString extension = originalFileInfo.suffix();
|
||||||
QString baseName = originalFileInfo.completeBaseName();
|
QString baseName = originalFileInfo.completeBaseName();
|
||||||
QString uniqueName = QString("%1_%2.%3")
|
QString uniqueName = QString("%1_%2.%3")
|
||||||
.arg(baseName)
|
.arg(baseName)
|
||||||
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
|
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
|
||||||
.arg(extension);
|
.arg(extension);
|
||||||
|
|
||||||
QString fullPath = QDir(imagesFolder).filePath(uniqueName);
|
QString fullPath = QDir(contentFolder).filePath(uniqueName);
|
||||||
|
|
||||||
QByteArray imageData = QByteArray::fromBase64(base64Data.toUtf8());
|
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
|
||||||
QFile file(fullPath);
|
QFile file(fullPath);
|
||||||
if (!file.open(QIODevice::WriteOnly)) {
|
if (!file.open(QIODevice::WriteOnly)) {
|
||||||
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
|
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.write(imageData) == -1) {
|
if (file.write(contentData) == -1) {
|
||||||
LOG_MESSAGE(QString("Failed to write image data: %1").arg(file.errorString()));
|
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
storedPath = uniqueName;
|
storedPath = uniqueName;
|
||||||
LOG_MESSAGE(QString("Saved image: %1 to %2").arg(fileName, fullPath));
|
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatSerializer::loadImageFromStorage(const QString &chatFilePath, const QString &storedPath)
|
QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath)
|
||||||
{
|
{
|
||||||
QString imagesFolder = getChatImagesFolder(chatFilePath);
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
QString fullPath = QDir(imagesFolder).filePath(storedPath);
|
QString fullPath = QDir(contentFolder).filePath(storedPath);
|
||||||
|
|
||||||
QFile file(fullPath);
|
QFile file(fullPath);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
LOG_MESSAGE(QString("Failed to open image file: %1").arg(fullPath));
|
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray imageData = file.readAll();
|
QByteArray contentData = file.readAll();
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
return imageData.toBase64();
|
return contentData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ public:
|
|||||||
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
||||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
||||||
|
|
||||||
// Image management
|
// Content management (images and text files)
|
||||||
static QString getChatImagesFolder(const QString &chatFilePath);
|
static QString getChatContentFolder(const QString &chatFilePath);
|
||||||
static bool saveImageToStorage(const QString &chatFilePath,
|
static bool saveContentToStorage(const QString &chatFilePath,
|
||||||
const QString &fileName,
|
const QString &fileName,
|
||||||
const QString &base64Data,
|
const QString &base64Data,
|
||||||
QString &storedPath);
|
QString &storedPath);
|
||||||
static QString loadImageFromStorage(const QString &chatFilePath, const QString &storedPath);
|
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString VERSION;
|
static const QString VERSION;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
|
#include <projectexplorer/buildconfiguration.h>
|
||||||
|
#include <projectexplorer/target.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -41,12 +43,12 @@
|
|||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "ToolsSettings.hpp"
|
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
#include "RequestConfig.hpp"
|
#include "RequestConfig.hpp"
|
||||||
#include <context/ChangesManager.h>
|
#include "ToolsSettings.hpp"
|
||||||
#include <RulesLoader.hpp>
|
#include <RulesLoader.hpp>
|
||||||
|
#include <context/ChangesManager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -67,7 +69,8 @@ void ClientInterface::sendMessage(
|
|||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments,
|
const QList<QString> &attachments,
|
||||||
const QList<QString> &linkedFiles,
|
const QList<QString> &linkedFiles,
|
||||||
bool useAgentMode)
|
bool useTools,
|
||||||
|
bool useThinking)
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
m_accumulatedResponses.clear();
|
||||||
@@ -85,7 +88,24 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto attachFiles = m_contextManager->getContentFiles(textFiles);
|
QList<Context::ContentFile> storedAttachments;
|
||||||
|
if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
|
auto attachFiles = m_contextManager->getContentFiles(textFiles);
|
||||||
|
for (const auto &file : attachFiles) {
|
||||||
|
QString storedPath;
|
||||||
|
if (ChatSerializer::saveContentToStorage(
|
||||||
|
m_chatFilePath, file.filename, file.content.toUtf8().toBase64(), storedPath)) {
|
||||||
|
Context::ContentFile storedFile;
|
||||||
|
storedFile.filename = file.filename;
|
||||||
|
storedFile.content = storedPath;
|
||||||
|
storedAttachments.append(storedFile);
|
||||||
|
LOG_MESSAGE(QString("Stored text file %1 as %2").arg(file.filename, storedPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!textFiles.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 text file(s)")
|
||||||
|
.arg(textFiles.size()));
|
||||||
|
}
|
||||||
|
|
||||||
QList<ChatModel::ImageAttachment> imageAttachments;
|
QList<ChatModel::ImageAttachment> imageAttachments;
|
||||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
@@ -97,7 +117,8 @@ void ClientInterface::sendMessage(
|
|||||||
|
|
||||||
QString storedPath;
|
QString storedPath;
|
||||||
QFileInfo fileInfo(imagePath);
|
QFileInfo fileInfo(imagePath);
|
||||||
if (ChatSerializer::saveImageToStorage(m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
if (ChatSerializer::saveContentToStorage(
|
||||||
|
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
||||||
ChatModel::ImageAttachment imageAttachment;
|
ChatModel::ImageAttachment imageAttachment;
|
||||||
imageAttachment.fileName = fileInfo.fileName();
|
imageAttachment.fileName = fileInfo.fileName();
|
||||||
imageAttachment.storedPath = storedPath;
|
imageAttachment.storedPath = storedPath;
|
||||||
@@ -108,10 +129,11 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!imageFiles.isEmpty()) {
|
} else if (!imageFiles.isEmpty()) {
|
||||||
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)").arg(imageFiles.size()));
|
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)")
|
||||||
|
.arg(imageFiles.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles, imageAttachments);
|
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
||||||
|
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
@@ -133,16 +155,31 @@ void ClientInterface::sendMessage(
|
|||||||
|
|
||||||
LLMCore::ContextData context;
|
LLMCore::ContextData context;
|
||||||
|
|
||||||
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode;
|
const bool isToolsEnabled = useTools;
|
||||||
|
|
||||||
if (chatAssistantSettings.useSystemPrompt()) {
|
if (chatAssistantSettings.useSystemPrompt()) {
|
||||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
|
|
||||||
|
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 = LLMCore::RulesLoader::getActiveProject();
|
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||||
|
|
||||||
if (project) {
|
if (project) {
|
||||||
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
||||||
systemPrompt += QString("\n# Active Project path: %1").arg(project->projectDirectory().toUrlishString());
|
systemPrompt += QString("\n# Active Project path: %1")
|
||||||
|
.arg(project->projectDirectory().toUrlishString());
|
||||||
|
|
||||||
|
if (auto target = project->activeTarget()) {
|
||||||
|
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||||
|
systemPrompt += QString("\n# Active Build directory: %1")
|
||||||
|
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString projectRules
|
QString projectRules
|
||||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
|
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
|
||||||
@@ -169,6 +206,19 @@ void ClientInterface::sendMessage(
|
|||||||
LLMCore::Message apiMessage;
|
LLMCore::Message apiMessage;
|
||||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
||||||
apiMessage.content = msg.content;
|
apiMessage.content = msg.content;
|
||||||
|
|
||||||
|
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
|
apiMessage.content += "\n\nAttached files:";
|
||||||
|
for (const auto &attachment : msg.attachments) {
|
||||||
|
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
||||||
|
if (!fileContent.isEmpty()) {
|
||||||
|
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||||
|
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
|
||||||
|
.arg(attachment.filename, decodedContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
||||||
apiMessage.isRedacted = msg.isRedacted;
|
apiMessage.isRedacted = msg.isRedacted;
|
||||||
apiMessage.signature = msg.signature;
|
apiMessage.signature = msg.signature;
|
||||||
@@ -215,8 +265,8 @@ void ClientInterface::sendMessage(
|
|||||||
promptTemplate,
|
promptTemplate,
|
||||||
context,
|
context,
|
||||||
LLMCore::RequestType::Chat,
|
LLMCore::RequestType::Chat,
|
||||||
isToolsEnabled,
|
useTools,
|
||||||
Settings::chatAssistantSettings().enableThinkingMode());
|
useThinking);
|
||||||
|
|
||||||
QString requestId = QUuid::createUuid().toString();
|
QString requestId = QUuid::createUuid().toString();
|
||||||
QJsonObject request{{"id", requestId}};
|
QJsonObject request{{"id", requestId}};
|
||||||
@@ -246,14 +296,14 @@ void ClientInterface::sendMessage(
|
|||||||
connect(
|
connect(
|
||||||
provider,
|
provider,
|
||||||
&LLMCore::Provider::toolExecutionStarted,
|
&LLMCore::Provider::toolExecutionStarted,
|
||||||
m_chatModel,
|
this,
|
||||||
&ChatModel::addToolExecutionStatus,
|
&ClientInterface::handleToolExecutionStarted,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider,
|
||||||
&LLMCore::Provider::toolExecutionCompleted,
|
&LLMCore::Provider::toolExecutionCompleted,
|
||||||
m_chatModel,
|
this,
|
||||||
&ChatModel::updateToolResult,
|
&ClientInterface::handleToolExecutionCompleted,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider,
|
||||||
@@ -264,23 +314,34 @@ void ClientInterface::sendMessage(
|
|||||||
connect(
|
connect(
|
||||||
provider,
|
provider,
|
||||||
&LLMCore::Provider::thinkingBlockReceived,
|
&LLMCore::Provider::thinkingBlockReceived,
|
||||||
m_chatModel,
|
this,
|
||||||
&ChatModel::addThinkingBlock,
|
&ClientInterface::handleThinkingBlockReceived,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider,
|
||||||
&LLMCore::Provider::redactedThinkingBlockReceived,
|
&LLMCore::Provider::redactedThinkingBlockReceived,
|
||||||
m_chatModel,
|
this,
|
||||||
&ChatModel::addRedactedThinkingBlock,
|
&ClientInterface::handleRedactedThinkingBlockReceived,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||||
|
|
||||||
|
if (provider->supportsTools() && provider->toolsManager()) {
|
||||||
|
provider->toolsManager()->setCurrentSessionId(m_chatFilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::clearMessages()
|
void ClientInterface::clearMessages()
|
||||||
{
|
{
|
||||||
|
const auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (provider && !m_chatFilePath.isEmpty() && provider->supportsTools()
|
||||||
|
&& provider->toolsManager()) {
|
||||||
|
provider->toolsManager()->clearTodoSession(m_chatFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
m_chatModel->clear();
|
m_chatModel->clear();
|
||||||
LOG_MESSAGE("Chat history cleared");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
@@ -388,12 +449,12 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
|||||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
||||||
|
|
||||||
QString applyError;
|
QString applyError;
|
||||||
bool applySuccess = Context::ChangesManager::instance()
|
bool applySuccess
|
||||||
.applyPendingEditsForRequest(requestId, &applyError);
|
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
|
||||||
|
|
||||||
if (!applySuccess) {
|
if (!applySuccess) {
|
||||||
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
|
||||||
.arg(requestId, applyError));
|
.arg(requestId, applyError));
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
@@ -432,11 +493,57 @@ void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
|
|||||||
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleThinkingBlockReceived(
|
||||||
|
const QString &requestId, const QString &thinking, const QString &signature)
|
||||||
|
{
|
||||||
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleRedactedThinkingBlockReceived(
|
||||||
|
const QString &requestId, const QString &signature)
|
||||||
|
{
|
||||||
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Ignoring redacted thinking block for non-chat request: %1").arg(requestId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleToolExecutionStarted(
|
||||||
|
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||||
|
{
|
||||||
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleToolExecutionCompleted(
|
||||||
|
const QString &requestId,
|
||||||
|
const QString &toolId,
|
||||||
|
const QString &toolName,
|
||||||
|
const QString &toolOutput)
|
||||||
|
{
|
||||||
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
|
||||||
|
}
|
||||||
|
|
||||||
bool ClientInterface::isImageFile(const QString &filePath) const
|
bool ClientInterface::isImageFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
static const QSet<QString> imageExtensions = {
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||||
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"
|
|
||||||
};
|
|
||||||
|
|
||||||
QFileInfo fileInfo(filePath);
|
QFileInfo fileInfo(filePath);
|
||||||
QString extension = fileInfo.suffix().toLower();
|
QString extension = fileInfo.suffix().toLower();
|
||||||
@@ -446,15 +553,14 @@ bool ClientInterface::isImageFile(const QString &filePath) const
|
|||||||
|
|
||||||
QString ClientInterface::getMediaTypeForImage(const QString &filePath) const
|
QString ClientInterface::getMediaTypeForImage(const QString &filePath) const
|
||||||
{
|
{
|
||||||
static const QHash<QString, QString> mediaTypes = {
|
static const QHash<QString, QString> mediaTypes
|
||||||
{"png", "image/png"},
|
= {{"png", "image/png"},
|
||||||
{"jpg", "image/jpeg"},
|
{"jpg", "image/jpeg"},
|
||||||
{"jpeg", "image/jpeg"},
|
{"jpeg", "image/jpeg"},
|
||||||
{"gif", "image/gif"},
|
{"gif", "image/gif"},
|
||||||
{"webp", "image/webp"},
|
{"webp", "image/webp"},
|
||||||
{"bmp", "image/bmp"},
|
{"bmp", "image/bmp"},
|
||||||
{"svg", "image/svg+xml"}
|
{"svg", "image/svg+xml"}};
|
||||||
};
|
|
||||||
|
|
||||||
QFileInfo fileInfo(filePath);
|
QFileInfo fileInfo(filePath);
|
||||||
QString extension = fileInfo.suffix().toLower();
|
QString extension = fileInfo.suffix().toLower();
|
||||||
@@ -482,12 +588,14 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
|||||||
return imageData.toBase64();
|
return imageData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const
|
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||||
|
const QList<ChatModel::ImageAttachment> &storedImages) const
|
||||||
{
|
{
|
||||||
QVector<LLMCore::ImageAttachment> apiImages;
|
QVector<LLMCore::ImageAttachment> apiImages;
|
||||||
|
|
||||||
for (const auto &storedImage : storedImages) {
|
for (const auto &storedImage : storedImages) {
|
||||||
QString base64Data = ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath);
|
QString base64Data
|
||||||
|
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
||||||
if (base64Data.isEmpty()) {
|
if (base64Data.isEmpty()) {
|
||||||
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
||||||
continue;
|
continue;
|
||||||
@@ -506,6 +614,15 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(const Q
|
|||||||
|
|
||||||
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 = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (provider && provider->supportsTools() && provider->toolsManager()) {
|
||||||
|
provider->toolsManager()->clearTodoSession(m_chatFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_chatFilePath = filePath;
|
m_chatFilePath = filePath;
|
||||||
m_chatModel->setChatFilePath(filePath);
|
m_chatModel->setChatFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ public:
|
|||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments = {},
|
const QList<QString> &attachments = {},
|
||||||
const QList<QString> &linkedFiles = {},
|
const QList<QString> &linkedFiles = {},
|
||||||
bool useAgentMode = false);
|
bool useTools = false,
|
||||||
|
bool useThinking = false);
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
@@ -62,6 +63,16 @@ private slots:
|
|||||||
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 handleCleanAccumulatedData(const QString &requestId);
|
||||||
|
void handleThinkingBlockReceived(
|
||||||
|
const QString &requestId, const QString &thinking, const QString &signature);
|
||||||
|
void handleRedactedThinkingBlockReceived(const QString &requestId, const QString &signature);
|
||||||
|
void handleToolExecutionStarted(
|
||||||
|
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||||
|
void handleToolExecutionCompleted(
|
||||||
|
const QString &requestId,
|
||||||
|
const QString &toolId,
|
||||||
|
const QString &toolName,
|
||||||
|
const QString &toolOutput);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||||
|
|||||||
442
ChatView/FileMentionItem.cpp
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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
|
||||||
86
ChatView/FileMentionItem.hpp
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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
|
||||||
10
ChatView/icons/compress-icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Archive/compress icon: downward arrows pointing to center with horizontal lines -->
|
||||||
|
<line x1="12" y1="3" x2="12" y2="10" />
|
||||||
|
<polyline points="9 7 12 10 15 7" />
|
||||||
|
|
||||||
|
<line x1="12" y1="21" x2="12" y2="14" />
|
||||||
|
<polyline points="9 17 12 14 15 17" />
|
||||||
|
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 487 B |
5
ChatView/icons/context-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
|
||||||
|
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 233 B |
4
ChatView/icons/settings-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 962 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
|
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black" fill-opacity="0.6"/>
|
||||||
<path d="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
<path d="M6 35L38 6" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
11
ChatView/icons/tools-icon-off.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_82_71)">
|
||||||
|
<path d="M10.7777 0.0435181C14.2316 -0.253961 17.6161 0.979215 20.0629 3.42633C23.4139 6.77767 24.3012 11.6719 22.7299 15.8433C22.9016 15.988 23.0706 16.1419 23.2377 16.3072L42.2221 34.2203C42.2288 34.2268 42.2353 34.2344 42.2426 34.2408C44.4752 36.4735 44.4752 40.1064 42.2426 42.3394C40.0096 44.5717 36.4035 44.5446 34.1713 42.3121C34.1617 42.3031 34.1528 42.2937 34.144 42.2838L16.3871 23.1519C16.2254 22.9894 16.0746 22.8196 15.933 22.6451C11.7604 24.2194 6.86327 23.3335 3.50919 19.98C1.06298 17.5327 -0.171482 14.1483 0.126373 10.6949C0.160109 10.3034 0.41818 9.96685 0.78653 9.83258C1.15602 9.69759 1.57009 9.78945 1.84805 10.067L7.53555 15.7535L13.8402 13.7574L15.8363 7.4527L10.1488 1.7652C9.87057 1.48716 9.77945 1.07345 9.91348 0.704651C10.0489 0.335072 10.3852 0.0774496 10.7777 0.0435181ZM37.3656 34.7496L37.3129 34.9302L37.1586 35.4673L36.8363 35.5679L36.6195 35.2047L36.4623 34.942L36.2357 35.148L35.725 35.6148L35.5746 35.7525L35.6791 35.9283L35.9184 36.3287L35.7104 36.6548L35.1742 36.5543L34.9408 36.5093L34.8852 36.7418L34.7572 37.275L34.7123 37.4644L34.8842 37.5543L35.3891 37.8179V38.1802L34.8842 38.4449L34.7123 38.5347L34.7572 38.7242L34.8852 39.2574L34.9408 39.4898L35.1742 39.4449L35.7104 39.3433L35.9184 39.6695L35.6791 40.0709L35.5746 40.2466L35.725 40.3843L36.2357 40.8511L36.4623 41.0572L36.6195 40.7945L36.8363 40.4302L37.1586 40.5308L37.3129 41.0689L37.3656 41.2496H38.6352L38.6879 41.0689L38.8412 40.5308L39.1635 40.4302L39.3813 40.7945L39.5385 41.0572L39.765 40.8511L40.2758 40.3843L40.4262 40.2466L40.3217 40.0709L40.0815 39.6695L40.2895 39.3433L40.8266 39.4449L41.06 39.4898L41.1156 39.2574L41.2436 38.7242L41.2885 38.5347L41.1166 38.4449L40.6117 38.1802V37.8179L41.1166 37.5543L41.2885 37.4644L41.2436 37.275L41.1156 36.7418L41.06 36.5093L40.8266 36.5543L40.2895 36.6548L40.0815 36.3287L40.3217 35.9283L40.4262 35.7525L40.2758 35.6148L39.765 35.148L39.5385 34.942L39.3813 35.2047L39.1635 35.5679L38.8412 35.4673L38.6879 34.9302L38.6352 34.7496H37.3656Z" fill="black" fill-opacity="0.6"/>
|
||||||
|
<path d="M6 36L38 7" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_82_71">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
10
ChatView/icons/tools-icon-on.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_82_50)">
|
||||||
|
<path d="M10.7775 0.0441895C14.2315 -0.253375 17.6159 0.979824 20.0627 3.427C23.4135 6.77842 24.3011 11.6726 22.7297 15.844C22.9013 15.9886 23.0714 16.1416 23.2385 16.3069L42.2219 34.2209C42.2285 34.2274 42.2352 34.2341 42.2424 34.2405C44.475 36.4732 44.475 40.1061 42.2424 42.3391C40.0094 44.5715 36.4033 44.5444 34.1711 42.3118C34.1615 42.3028 34.1525 42.2934 34.1437 42.2834L16.3869 23.1516C16.2251 22.9891 16.0745 22.8193 15.9328 22.6448C11.7602 24.2191 6.86304 23.3333 3.50897 19.9797C1.06276 17.5324 -0.171773 14.148 0.12616 10.6946C0.159908 10.3029 0.418723 9.96644 0.787292 9.83228C1.15667 9.69748 1.56997 9.78926 1.84784 10.0667L7.53534 15.7532L13.84 13.7571L15.8361 7.45239L10.1486 1.76489C9.87052 1.48684 9.78022 1.07306 9.91425 0.704346C10.0498 0.334991 10.3852 0.0781082 10.7775 0.0441895ZM37.3654 34.7502L37.3127 34.9309L37.1584 35.468L36.8361 35.5686L36.6193 35.2053L36.4621 34.9426L36.2355 35.1487L35.7248 35.6155L35.5744 35.7532L35.6789 35.929L35.9182 36.3293L35.7101 36.6555L35.174 36.5549L34.9406 36.51L34.8849 36.7424L34.757 37.2756L34.7121 37.4651L34.884 37.5549L35.3889 37.8186V38.1809L34.884 38.4456L34.7121 38.5354L34.757 38.7249L34.8849 39.2581L34.9406 39.4905L35.174 39.4456L35.7101 39.344L35.9182 39.6702L35.6789 40.0715L35.5744 40.2473L35.7248 40.385L36.2355 40.8518L36.4621 41.0579L36.6193 40.7952L36.8361 40.4309L37.1584 40.5315L37.3127 41.0696L37.3654 41.2502H38.6349L38.6877 41.0696L38.841 40.5315L39.1633 40.4309L39.381 40.7952L39.5383 41.0579L39.7648 40.8518L40.2756 40.385L40.426 40.2473L40.3215 40.0715L40.0812 39.6702L40.2892 39.344L40.8264 39.4456L41.0598 39.4905L41.1154 39.2581L41.2433 38.7249L41.2883 38.5354L41.1164 38.4456L40.6115 38.1809V37.8186L41.1164 37.5549L41.2883 37.4651L41.2433 37.2756L41.1154 36.7424L41.0598 36.51L40.8264 36.5549L40.2892 36.6555L40.0812 36.3293L40.3215 35.929L40.426 35.7532L40.2756 35.6155L39.7648 35.1487L39.5383 34.9426L39.381 35.2053L39.1633 35.5686L38.841 35.468L38.6877 34.9309L38.6349 34.7502H37.3654Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_82_50">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -26,6 +26,7 @@ import UIControls
|
|||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|
||||||
import "./chatparts"
|
import "./chatparts"
|
||||||
|
import "./controls"
|
||||||
|
|
||||||
ChatRootView {
|
ChatRootView {
|
||||||
id: root
|
id: root
|
||||||
@@ -60,6 +61,7 @@ ChatRootView {
|
|||||||
|
|
||||||
SplitDropZone {
|
SplitDropZone {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
z: 99
|
||||||
|
|
||||||
onFilesDroppedToAttach: (urlStrings) => {
|
onFilesDroppedToAttach: (urlStrings) => {
|
||||||
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
||||||
@@ -86,9 +88,12 @@ ChatRootView {
|
|||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: childrenRect.height + 10
|
Layout.preferredHeight: childrenRect.height + 10
|
||||||
|
|
||||||
|
isCompressing: root.isCompressing
|
||||||
saveButton.onClicked: root.showSaveDialog()
|
saveButton.onClicked: root.showSaveDialog()
|
||||||
loadButton.onClicked: root.showLoadDialog()
|
loadButton.onClicked: root.showLoadDialog()
|
||||||
clearButton.onClicked: root.clearChat()
|
clearButton.onClicked: root.clearChat()
|
||||||
|
compressButton.onClicked: compressConfirmDialog.open()
|
||||||
|
cancelCompressButton.onClicked: root.cancelCompression()
|
||||||
tokensBadge {
|
tokensBadge {
|
||||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||||
}
|
}
|
||||||
@@ -96,27 +101,26 @@ ChatRootView {
|
|||||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||||
}
|
}
|
||||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||||
rulesButton.onClicked: rulesViewer.open()
|
contextButton.onClicked: contextViewer.open()
|
||||||
activeRulesCount: root.activeRulesCount
|
|
||||||
pinButton {
|
pinButton {
|
||||||
visible: typeof _chatview !== 'undefined'
|
visible: typeof _chatview !== 'undefined'
|
||||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||||
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
||||||
}
|
}
|
||||||
agentModeSwitch {
|
toolsButton {
|
||||||
checked: root.isAgentMode
|
checked: root.useTools
|
||||||
enabled: root.toolsSupportEnabled
|
onCheckedChanged: {
|
||||||
onToggled: {
|
root.useTools = toolsButton.checked
|
||||||
root.isAgentMode = agentModeSwitch.checked
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
thinkingMode {
|
thinkingMode {
|
||||||
checked: root.isThinkingMode
|
checked: root.useThinking
|
||||||
enabled: root.isThinkingSupport
|
enabled: root.isThinkingSupport
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
root.isThinkingMode = thinkingMode.checked
|
root.useThinking = thinkingMode.checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
settingsButton.onClicked: root.openSettings()
|
||||||
configSelector {
|
configSelector {
|
||||||
model: root.availableConfigurations
|
model: root.availableConfigurations
|
||||||
displayText: root.currentConfiguration
|
displayText: root.currentConfiguration
|
||||||
@@ -130,12 +134,24 @@ ChatRootView {
|
|||||||
root.loadAvailableConfigurations()
|
root.loadAvailableConfigurations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roleSelector {
|
||||||
|
model: root.availableAgentRoles
|
||||||
|
displayText: root.currentAgentRole
|
||||||
|
onActivated: function(index) {
|
||||||
|
root.applyAgentRole(root.availableAgentRoles[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.onAboutToShow: {
|
||||||
|
root.loadAvailableAgentRoles()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: chatListView
|
id: chatListView
|
||||||
|
|
||||||
signal hideServiceComponents(int itemIndex)
|
property bool userScrolledUp: false
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
@@ -146,6 +162,18 @@ ChatRootView {
|
|||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
cacheBuffer: 2000
|
cacheBuffer: 2000
|
||||||
|
|
||||||
|
onMovingChanged: {
|
||||||
|
if (moving) {
|
||||||
|
userScrolledUp = !atYEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAtYEndChanged: {
|
||||||
|
if (atYEnd) {
|
||||||
|
userScrolledUp = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: Loader {
|
delegate: Loader {
|
||||||
id: componentLoader
|
id: componentLoader
|
||||||
|
|
||||||
@@ -166,11 +194,6 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (componentLoader.sourceComponent == chatItemComponent) {
|
|
||||||
chatListView.hideServiceComponents(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header: Item {
|
header: Item {
|
||||||
@@ -182,12 +205,53 @@ ChatRootView {
|
|||||||
id: scroll
|
id: scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: scrollToBottomButton
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
bottom: parent.bottom
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
bottomMargin: 10
|
||||||
|
}
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: palette.button
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
visible: chatListView.userScrolledUp
|
||||||
|
opacity: 0.9
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "▼"
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: palette.buttonText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
chatListView.userScrolledUp = false
|
||||||
|
root.scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on visible {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onCountChanged: {
|
onCountChanged: {
|
||||||
root.scrollToBottom()
|
if (!userScrolledUp) {
|
||||||
|
root.scrollToBottom()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentHeightChanged: {
|
onContentHeightChanged: {
|
||||||
if (atYEnd) {
|
if (!userScrolledUp && atYEnd) {
|
||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,6 +280,10 @@ ChatRootView {
|
|||||||
messageInput.cursorPosition = model.content.length
|
messageInput.cursorPosition = model.content.length
|
||||||
root.chatModel.resetModelTo(idx)
|
root.chatModel.resetModelTo(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenFileRequested: function(filePath) {
|
||||||
|
root.openFileInEditor(filePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,19 +291,8 @@ ChatRootView {
|
|||||||
id: toolMessageComponent
|
id: toolMessageComponent
|
||||||
|
|
||||||
ToolBlock {
|
ToolBlock {
|
||||||
id: toolsItem
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
toolContent: model.content
|
toolContent: model.content
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: chatListView
|
|
||||||
function onHideServiceComponents(itemIndex) {
|
|
||||||
if (index !== itemIndex) {
|
|
||||||
toolsItem.headerOpacity = 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +325,6 @@ ChatRootView {
|
|||||||
id: thinkingMessageComponent
|
id: thinkingMessageComponent
|
||||||
|
|
||||||
ThinkingBlock {
|
ThinkingBlock {
|
||||||
id: thinking
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
thinkingContent: {
|
thinkingContent: {
|
||||||
let content = model.content
|
let content = model.content
|
||||||
@@ -280,15 +335,6 @@ ChatRootView {
|
|||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
|
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: chatListView
|
|
||||||
function onHideServiceComponents(itemIndex) {
|
|
||||||
if (index !== itemIndex) {
|
|
||||||
thinking.headerOpacity = 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,7 +372,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
|
||||||
@@ -437,15 +514,12 @@ ChatRootView {
|
|||||||
|
|
||||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||||
context: Qt.WindowShortcut
|
context: Qt.WindowShortcut
|
||||||
onActivated: {
|
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
|
||||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
onActivated: root.sendChatMessage()
|
||||||
root.sendChatMessage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.chatModel.clear()
|
root.clearMessages()
|
||||||
root.clearAttachmentFiles()
|
root.clearAttachmentFiles()
|
||||||
root.updateInputTokensCount()
|
root.updateInputTokensCount()
|
||||||
}
|
}
|
||||||
@@ -454,12 +528,38 @@ ChatRootView {
|
|||||||
Qt.callLater(chatListView.positionViewAtEnd)
|
Qt.callLater(chatListView.positionViewAtEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyMentionSelection() {
|
||||||
|
var result = fileMentionPopup.applyCurrentSelection(
|
||||||
|
messageInput.text, messageInput.cursorPosition, root.useTools)
|
||||||
|
if (result.text !== undefined) {
|
||||||
|
messageInput.text = result.text
|
||||||
|
messageInput.cursorPosition = result.cursorPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.sendMessage(messageInput.text)
|
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
|
fileMentionPopup.clearMentions()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: compressConfirmDialog
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
title: qsTr("Compress Chat")
|
||||||
|
modal: true
|
||||||
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: root.compressCurrentChat()
|
||||||
|
}
|
||||||
|
|
||||||
Toast {
|
Toast {
|
||||||
id: errorToast
|
id: errorToast
|
||||||
z: 1000
|
z: 1000
|
||||||
@@ -478,19 +578,28 @@ ChatRootView {
|
|||||||
toastTextColor: "#FFFFFF"
|
toastTextColor: "#FFFFFF"
|
||||||
}
|
}
|
||||||
|
|
||||||
RulesViewer {
|
ContextViewer {
|
||||||
id: rulesViewer
|
id: contextViewer
|
||||||
|
|
||||||
width: parent.width * 0.8
|
width: Math.min(parent.width * 0.85, 800)
|
||||||
height: parent.height * 0.8
|
height: Math.min(parent.height * 0.85, 700)
|
||||||
x: (parent.width - width) / 2
|
x: (parent.width - width) / 2
|
||||||
y: (parent.height - height) / 2
|
y: (parent.height - height) / 2
|
||||||
|
|
||||||
|
baseSystemPrompt: root.baseSystemPrompt
|
||||||
|
currentAgentRole: root.currentAgentRole
|
||||||
|
currentAgentRoleDescription: root.currentAgentRoleDescription
|
||||||
|
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
||||||
activeRules: root.activeRules
|
activeRules: root.activeRules
|
||||||
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex)
|
activeRulesCount: root.activeRulesCount
|
||||||
|
|
||||||
onRefreshRules: root.refreshRules()
|
onOpenSettings: root.openSettings()
|
||||||
|
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||||
onOpenRulesFolder: root.openRulesFolder()
|
onOpenRulesFolder: root.openRulesFolder()
|
||||||
|
onRefreshRules: root.refreshRules()
|
||||||
|
onRuleSelected: function(index) {
|
||||||
|
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -505,6 +614,26 @@ ChatRootView {
|
|||||||
infoToast.show(root.lastInfoMessage)
|
infoToast.show(root.lastInfoMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function onOpenFilesChanged() {
|
||||||
|
if (fileMentionPopup.visible)
|
||||||
|
Qt.callLater(fileMentionPopup.refreshSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMentionPopup {
|
||||||
|
id: fileMentionPopup
|
||||||
|
|
||||||
|
z: 999
|
||||||
|
width: Math.min(480, root.width - 20)
|
||||||
|
|
||||||
|
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
|
||||||
|
y: view.y - height - 4
|
||||||
|
|
||||||
|
onSelectionRequested: root.applyMentionSelection()
|
||||||
|
|
||||||
|
onFileAttachRequested: function(filePaths) {
|
||||||
|
root.addFilesToAttachList(filePaths)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -51,6 +51,7 @@ Rectangle {
|
|||||||
property int messageIndex: -1
|
property int messageIndex: -1
|
||||||
|
|
||||||
signal resetChatToMessage(int index)
|
signal resetChatToMessage(int index)
|
||||||
|
signal openFileRequested(string filePath)
|
||||||
|
|
||||||
height: msgColumn.implicitHeight + 10
|
height: msgColumn.implicitHeight + 10
|
||||||
radius: 8
|
radius: 8
|
||||||
@@ -121,24 +122,11 @@ Rectangle {
|
|||||||
Repeater {
|
Repeater {
|
||||||
id: attachmentsModel
|
id: attachmentsModel
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: AttachmentComponent {
|
||||||
required property int index
|
required property int index
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
height: attachText.implicitHeight + 8
|
itemData: modelData
|
||||||
width: attachText.implicitWidth + 16
|
|
||||||
radius: 4
|
|
||||||
color: palette.button
|
|
||||||
border.width: 1
|
|
||||||
border.color: palette.mid
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: attachText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,9 +181,12 @@ Rectangle {
|
|||||||
onClicked: function() {
|
onClicked: function() {
|
||||||
root.resetChatToMessage(root.messageIndex)
|
root.resetChatToMessage(root.messageIndex)
|
||||||
}
|
}
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("Reset chat to this message and edit")
|
QoAToolTip {
|
||||||
ToolTip.delay: 500
|
visible: stopButtonId.hovered
|
||||||
|
text: qsTr("Reset chat to this message and edit")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component TextComponent : TextBlock {
|
component TextComponent : TextBlock {
|
||||||
@@ -217,6 +208,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLinkActivated: function(link) {
|
||||||
|
if (link.startsWith("file://")) {
|
||||||
|
var filePath = link.replace(/^file:\/\//, "")
|
||||||
|
root.openFileRequested(filePath)
|
||||||
|
} else {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ChatUtils {
|
ChatUtils {
|
||||||
id: utils
|
id: utils
|
||||||
}
|
}
|
||||||
@@ -239,6 +239,56 @@ Rectangle {
|
|||||||
codeFontSize: root.codeFontSize
|
codeFontSize: root.codeFontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component AttachmentComponent : Rectangle {
|
||||||
|
required property var itemData
|
||||||
|
|
||||||
|
height: attachFileText.implicitHeight + 8
|
||||||
|
width: attachFileText.implicitWidth + 16
|
||||||
|
radius: 4
|
||||||
|
color: attachFileMouseArea.containsMouse ? Qt.lighter(palette.button, 1.1) : palette.button
|
||||||
|
border.width: 1
|
||||||
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
FileItem {
|
||||||
|
id: fileItem
|
||||||
|
filePath: itemData.filePath || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: attachFileText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (itemData.fileName || "")
|
||||||
|
color: palette.buttonText
|
||||||
|
font.pointSize: root.textFontSize - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: attachFileMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
fileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
|
fileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: attachFileMouseArea.containsMouse
|
||||||
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
component ImageComponent : Rectangle {
|
component ImageComponent : Rectangle {
|
||||||
required property var itemData
|
required property var itemData
|
||||||
|
|
||||||
@@ -248,10 +298,17 @@ Rectangle {
|
|||||||
width: Math.min(imageDisplay.implicitWidth, maxImageWidth) + 16
|
width: Math.min(imageDisplay.implicitWidth, maxImageWidth) + 16
|
||||||
height: imageDisplay.implicitHeight + fileNameText.implicitHeight + 16
|
height: imageDisplay.implicitHeight + fileNameText.implicitHeight + 16
|
||||||
radius: 4
|
radius: 4
|
||||||
color: palette.base
|
color: imageMouseArea.containsMouse ? Qt.lighter(palette.base, 1.05) : palette.base
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: palette.mid
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
FileItem {
|
||||||
|
id: imageFileItem
|
||||||
|
filePath: itemData.filePath || ""
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 8
|
anchors.margins: 8
|
||||||
@@ -259,6 +316,7 @@ Rectangle {
|
|||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: imageDisplay
|
id: imageDisplay
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.maximumWidth: parent.parent.maxImageWidth
|
Layout.maximumWidth: parent.parent.maxImageWidth
|
||||||
Layout.maximumHeight: parent.parent.maxImageHeight
|
Layout.maximumHeight: parent.parent.maxImageHeight
|
||||||
@@ -289,6 +347,7 @@ Rectangle {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: fileNameText
|
id: fileNameText
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: itemData.fileName || ""
|
text: itemData.fileName || ""
|
||||||
color: palette.text
|
color: palette.text
|
||||||
@@ -297,5 +356,28 @@ Rectangle {
|
|||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: imageMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
imageFileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
|
imageFileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: imageMouseArea.containsMouse
|
||||||
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -29,8 +29,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,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024-2025 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -21,6 +21,7 @@ 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 +79,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 {
|
||||||
|
|||||||
558
ChatView/qml/controls/ContextViewer.qml
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
/*
|
||||||
|
* 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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
ChatView/qml/controls/FileMentionPopup.qml
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 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
|
||||||
|
|
||||||
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,10 +23,12 @@ import QtQuick.Controls
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
signal filesDroppedToAttach(var urlStrings) // Array of URL strings (file://...)
|
signal filesDroppedToAttach(var urlStrings)
|
||||||
signal filesDroppedToLink(var urlStrings) // Array of URL strings (file://...)
|
signal filesDroppedToLink(var urlStrings)
|
||||||
|
|
||||||
property string activeZone: ""
|
property string activeZone: ""
|
||||||
|
property int filesCount: 0
|
||||||
|
property bool isDragActive: false
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: splitDropOverlay
|
id: splitDropOverlay
|
||||||
@@ -34,12 +36,39 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: false
|
visible: false
|
||||||
z: 999
|
z: 999
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
topMargin: 30
|
||||||
|
}
|
||||||
|
width: fileCountText.width + 40
|
||||||
|
height: 50
|
||||||
|
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
|
||||||
|
radius: 25
|
||||||
|
visible: root.filesCount > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: fileCountText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("%n file(s) to drop", "", root.filesCount)
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: palette.highlightedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: leftZone
|
id: leftZone
|
||||||
|
|
||||||
@@ -76,19 +105,20 @@ Item {
|
|||||||
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(for one-time use)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
ColorAnimation { duration: 150 }
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
}
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -127,19 +157,20 @@ Item {
|
|||||||
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
opacity: 0.8
|
opacity: 0.8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(added to context)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
ColorAnimation { duration: 150 }
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
}
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
|
||||||
Behavior on border.width {
|
|
||||||
NumberAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on border.color {
|
|
||||||
ColorAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -193,42 +224,67 @@ Item {
|
|||||||
|
|
||||||
onEntered: (drag) => {
|
onEntered: (drag) => {
|
||||||
if (drag.hasUrls) {
|
if (drag.hasUrls) {
|
||||||
|
root.isDragActive = true
|
||||||
|
root.filesCount = drag.urls.length
|
||||||
splitDropOverlay.visible = true
|
splitDropOverlay.visible = true
|
||||||
|
splitDropOverlay.opacity = 1
|
||||||
root.activeZone = ""
|
root.activeZone = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: {
|
onExited: {
|
||||||
splitDropOverlay.visible = false
|
root.isDragActive = false
|
||||||
root.activeZone = ""
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
|
Qt.callLater(function() {
|
||||||
|
if (!root.isDragActive) {
|
||||||
|
splitDropOverlay.visible = false
|
||||||
|
root.activeZone = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onPositionChanged: (drag) => {
|
onPositionChanged: (drag) => {
|
||||||
if (drag.x < globalDropArea.width / 2) {
|
if (drag.hasUrls) {
|
||||||
root.activeZone = "left"
|
root.activeZone = drag.x < globalDropArea.width / 2 ? "left" : "right"
|
||||||
} else {
|
|
||||||
root.activeZone = "right"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDropped: (drop) => {
|
onDropped: (drop) => {
|
||||||
var targetZone = root.activeZone
|
const targetZone = root.activeZone
|
||||||
splitDropOverlay.visible = false
|
root.isDragActive = false
|
||||||
root.activeZone = ""
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
if (drop.hasUrls && drop.urls.length > 0) {
|
Qt.callLater(function() {
|
||||||
// Convert URLs to array of strings for C++ processing
|
splitDropOverlay.visible = false
|
||||||
var urlStrings = []
|
root.activeZone = ""
|
||||||
for (var i = 0; i < drop.urls.length; i++) {
|
})
|
||||||
urlStrings.push(drop.urls[i].toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetZone === "right") {
|
if (!drop.hasUrls || drop.urls.length === 0) {
|
||||||
root.filesDroppedToLink(urlStrings)
|
return
|
||||||
} else {
|
}
|
||||||
root.filesDroppedToAttach(urlStrings)
|
|
||||||
|
var urlStrings = []
|
||||||
|
for (var i = 0; i < drop.urls.length; i++) {
|
||||||
|
var urlString = drop.urls[i].toString()
|
||||||
|
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
|
||||||
|
urlStrings.push(urlString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (urlStrings.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
drop.accept(Qt.CopyAction)
|
||||||
|
|
||||||
|
if (targetZone === "right") {
|
||||||
|
root.filesDroppedToLink(urlStrings)
|
||||||
|
} else {
|
||||||
|
root.filesDroppedToAttach(urlStrings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,15 +29,20 @@ Rectangle {
|
|||||||
property alias saveButton: saveButtonId
|
property alias saveButton: saveButtonId
|
||||||
property alias loadButton: loadButtonId
|
property alias loadButton: loadButtonId
|
||||||
property alias clearButton: clearButtonId
|
property alias clearButton: clearButtonId
|
||||||
|
property alias compressButton: compressButtonId
|
||||||
|
property alias cancelCompressButton: cancelCompressButtonId
|
||||||
property alias tokensBadge: tokensBadgeId
|
property alias tokensBadge: tokensBadgeId
|
||||||
property alias recentPath: recentPathId
|
property alias recentPath: recentPathId
|
||||||
property alias openChatHistory: openChatHistoryId
|
property alias openChatHistory: openChatHistoryId
|
||||||
property alias pinButton: pinButtonId
|
property alias pinButton: pinButtonId
|
||||||
property alias rulesButton: rulesButtonId
|
property alias contextButton: contextButtonId
|
||||||
property alias agentModeSwitch: agentModeSwitchId
|
property alias toolsButton: toolsButtonId
|
||||||
property alias thinkingMode: thinkingModeId
|
property alias thinkingMode: thinkingModeId
|
||||||
property alias activeRulesCount: activeRulesCountId.text
|
property alias settingsButton: settingsButtonId
|
||||||
property alias configSelector: configSelectorId
|
property alias configSelector: configSelectorId
|
||||||
|
property alias roleSelector: roleSelector
|
||||||
|
|
||||||
|
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) :
|
||||||
@@ -53,7 +58,8 @@ Rectangle {
|
|||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
height: agentModeSwitchId.height
|
id: firstRow
|
||||||
|
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -75,23 +81,61 @@ Rectangle {
|
|||||||
: qsTr("Pin chat window to the top")
|
: qsTr("Pin chat window to the top")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoATextSlider {
|
QoAComboBox {
|
||||||
id: agentModeSwitchId
|
id: configSelectorId
|
||||||
|
|
||||||
|
implicitHeight: 25
|
||||||
|
|
||||||
|
model: []
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Switch saved AI configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAComboBox {
|
||||||
|
id: roleSelector
|
||||||
|
|
||||||
|
implicitHeight: 25
|
||||||
|
|
||||||
|
model: []
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Switch agent role (different system prompts)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: toolsButtonId
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
leftText: "chat"
|
checkable: true
|
||||||
rightText: "AI Agent"
|
opacity: enabled ? 1.0 : 0.2
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
|
||||||
|
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: {
|
ToolTip.text: {
|
||||||
if (!agentModeSwitchId.enabled) {
|
if (!toolsButtonId.enabled) {
|
||||||
return qsTr("Tools are disabled in General Settings")
|
return qsTr("Tools are disabled in General Settings")
|
||||||
}
|
}
|
||||||
return checked
|
return checked
|
||||||
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
|
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
||||||
: qsTr("Chat Mode: Simple conversation without tool access")
|
: qsTr("Tools disabled: Simple conversation without tool access")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +161,32 @@ Rectangle {
|
|||||||
: qsTr("Thinking Mode disabled"))
|
: qsTr("Thinking Mode disabled"))
|
||||||
: qsTr("Thinking Mode is not available for this provider")
|
: qsTr("Thinking Mode is not available for this provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: settingsButtonId
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/settings-icon.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoASeparator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
height: agentModeSwitchId.height
|
height: firstRow.height
|
||||||
width: recentPathId.width
|
width: recentPathId.width
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -144,7 +210,10 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
id: secondRow
|
||||||
|
|
||||||
Layout.preferredWidth: root.width
|
Layout.preferredWidth: root.width
|
||||||
|
Layout.preferredHeight: firstRow.height
|
||||||
|
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
@@ -174,19 +243,6 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Load chat from *.json file")
|
ToolTip.text: qsTr("Load chat from *.json file")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: clearButtonId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
|
||||||
height: 15
|
|
||||||
width: 8
|
|
||||||
}
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.delay: 250
|
|
||||||
ToolTip.text: qsTr("Clean chat")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: openChatHistoryId
|
id: openChatHistoryId
|
||||||
|
|
||||||
@@ -200,36 +256,70 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Show in system")
|
ToolTip.text: qsTr("Show in system")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoASeparator {}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: rulesButtonId
|
id: compressButtonId
|
||||||
|
|
||||||
|
visible: !root.isCompressing
|
||||||
|
|
||||||
icon {
|
icon {
|
||||||
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
|
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)")
|
||||||
|
}
|
||||||
|
|
||||||
|
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...")
|
||||||
|
height: parent.height
|
||||||
|
color: palette.text
|
||||||
|
font.pixelSize: 12
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: cancelCompressButtonId
|
||||||
|
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Cancel compression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: contextButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
text: " "
|
|
||||||
|
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: root.activeRulesCount > 0
|
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
|
||||||
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
|
|
||||||
: qsTr("View active project rules (no rules found)")
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: activeRulesCountId
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
bottom: parent.bottom
|
|
||||||
bottomMargin: 2
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
color: palette.text
|
|
||||||
font.pixelSize: 10
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Badge {
|
Badge {
|
||||||
@@ -240,15 +330,19 @@ Rectangle {
|
|||||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoASeparator {}
|
||||||
id: configSelectorId
|
|
||||||
|
|
||||||
model: []
|
QoAButton {
|
||||||
currentIndex: 0
|
id: clearButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: qsTr("Switch AI configuration")
|
ToolTip.text: qsTr("Clean chat")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,28 +170,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->supportsModelListing()) {
|
||||||
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:"));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,8 +125,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
QString requestId = request["id"].toString();
|
QString requestId = request["id"].toString();
|
||||||
m_performanceLogger.startTimeMeasurement(requestId);
|
m_performanceLogger.startTimeMeasurement(requestId);
|
||||||
handleCompletion(request);
|
handleCompletion(request);
|
||||||
} else if (method == "cancelRequest") {
|
} else if (method == "$/cancelRequest") {
|
||||||
qDebug() << "Cancelling request";
|
|
||||||
handleCancelRequest();
|
handleCancelRequest();
|
||||||
} else if (method == "exit") {
|
} else if (method == "exit") {
|
||||||
// TODO make exit handler
|
// TODO make exit handler
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.9.1",
|
"Version" : "0.9.11",
|
||||||
"CompatVersion" : "${IDE_VERSION}",
|
"CompatVersion" : "${IDE_VERSION}",
|
||||||
"Vendor" : "Petr Mironychev",
|
"Vendor" : "Petr Mironychev",
|
||||||
"VendorId" : "petrmironychev",
|
"VendorId" : "petrmironychev",
|
||||||
|
|||||||
@@ -40,6 +40,9 @@
|
|||||||
#include "settings/CodeCompletionSettings.hpp"
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProjectSettings.hpp"
|
#include "settings/ProjectSettings.hpp"
|
||||||
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
|
#include "RefactorContextHelper.hpp"
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
|
|
||||||
@@ -71,12 +74,14 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
|||||||
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
|
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
|
||||||
|
|
||||||
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
||||||
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
delete m_refactorHoverHandler;
|
delete m_refactorHoverHandler;
|
||||||
|
delete m_refactorWidgetHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||||
@@ -451,11 +456,25 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditorWidget *editorWidget = result.editor;
|
int displayMode = Settings::quickRefactorSettings().displayMode();
|
||||||
|
|
||||||
auto toTextPos = [](const Utils::Text::Position &pos) {
|
if (displayMode == 0) {
|
||||||
return Utils::Text::Position{pos.line, pos.column};
|
displayRefactoringWidget(result);
|
||||||
};
|
} else {
|
||||||
|
displayRefactoringSuggestion(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)
|
||||||
|
{
|
||||||
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
|
|
||||||
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
||||||
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
|
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
|
||||||
@@ -510,6 +529,81 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
|||||||
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
|
||||||
|
{
|
||||||
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
|
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
||||||
|
|
||||||
|
RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
|
||||||
|
|
||||||
|
QString displayOriginal;
|
||||||
|
QString displayRefactored;
|
||||||
|
QString textToApply = result.newText;
|
||||||
|
|
||||||
|
if (ctx.isInsertion) {
|
||||||
|
bool isMultiline = result.newText.contains('\n');
|
||||||
|
|
||||||
|
if (isMultiline) {
|
||||||
|
displayOriginal = ctx.textBeforeCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText;
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
|
||||||
|
textToApply = result.newText;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.originalText;
|
||||||
|
displayRefactored = result.newText;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
|
||||||
|
applyRefactoringEdit(editorWidget, result.insertRange, editedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setDeclineCallback([]() {});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->showRefactorWidget(
|
||||||
|
editorWidget, displayOriginal, displayRefactored, range,
|
||||||
|
ctx.contextBefore, ctx.contextAfter);
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setTextToApply(textToApply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
const QString &text)
|
||||||
|
{
|
||||||
|
const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
|
||||||
|
const QTextCursor endCursor = range.end.toTextCursor(editor->document());
|
||||||
|
const int startPos = startCursor.position();
|
||||||
|
const int endPos = endCursor.position();
|
||||||
|
|
||||||
|
QTextCursor editCursor(editor->document());
|
||||||
|
editCursor.beginEditBlock();
|
||||||
|
|
||||||
|
if (startPos == endPos) {
|
||||||
|
bool isMultiline = text.contains('\n');
|
||||||
|
editCursor.setPosition(startPos);
|
||||||
|
|
||||||
|
if (isMultiline) {
|
||||||
|
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.insertText(text);
|
||||||
|
} else {
|
||||||
|
editCursor.setPosition(startPos);
|
||||||
|
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
editCursor.insertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.endEditBlock();
|
||||||
|
}
|
||||||
|
|
||||||
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
||||||
int charsAdded,
|
int charsAdded,
|
||||||
bool isSpaceOrTab)
|
bool isSpaceOrTab)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#include "widgets/CompletionErrorHandler.hpp"
|
#include "widgets/CompletionErrorHandler.hpp"
|
||||||
#include "widgets/CompletionHintHandler.hpp"
|
#include "widgets/CompletionHintHandler.hpp"
|
||||||
#include "widgets/EditorChatButtonHandler.hpp"
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
#include <languageclient/client.h>
|
#include <languageclient/client.h>
|
||||||
#include <llmcore/IPromptProvider.hpp>
|
#include <llmcore/IPromptProvider.hpp>
|
||||||
#include <llmcore/IProviderRegistry.hpp>
|
#include <llmcore/IProviderRegistry.hpp>
|
||||||
@@ -70,6 +71,9 @@ private:
|
|||||||
void setupConnections();
|
void setupConnections();
|
||||||
void cleanupConnections();
|
void cleanupConnections();
|
||||||
void handleRefactoringResult(const RefactorResult &result);
|
void handleRefactoringResult(const RefactorResult &result);
|
||||||
|
void displayRefactoringSuggestion(const RefactorResult &result);
|
||||||
|
void displayRefactoringWidget(const RefactorResult &result);
|
||||||
|
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
||||||
|
|
||||||
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
|
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
|
||||||
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
|
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
|
||||||
@@ -88,6 +92,7 @@ private:
|
|||||||
EditorChatButtonHandler m_chatButtonHandler;
|
EditorChatButtonHandler m_chatButtonHandler;
|
||||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||||
|
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||||
LLMClientInterface *m_llmClient;
|
LLMClientInterface *m_llmClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#include <context/DocumentContextReader.hpp>
|
#include <context/DocumentContextReader.hpp>
|
||||||
|
#include <llmcore/ResponseCleaner.hpp>
|
||||||
#include <context/DocumentReaderQtCreator.hpp>
|
#include <context/DocumentReaderQtCreator.hpp>
|
||||||
#include <context/Utils.hpp>
|
#include <context/Utils.hpp>
|
||||||
#include <llmcore/PromptTemplateManager.hpp>
|
#include <llmcore/PromptTemplateManager.hpp>
|
||||||
@@ -301,19 +302,67 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
|||||||
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
|
||||||
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
systemPrompt += "\nFile path: " + documentInfo.filePath;
|
||||||
|
|
||||||
systemPrompt += "\n\nCode context with position markers:";
|
systemPrompt += "\n\n# Code Context with Position Markers\n" + taggedContent;
|
||||||
systemPrompt += taggedContent;
|
|
||||||
|
|
||||||
systemPrompt += "\n\nOutput format:";
|
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
|
||||||
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
|
systemPrompt += cursor.hasSelection()
|
||||||
"between<selection_start><selection_end> or be "
|
? "\n- Generate ONLY the code that should REPLACE the selected text between "
|
||||||
"inserted at cursor position<cursor>";
|
"<selection_start> and <selection_end> markers"
|
||||||
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
|
"\n- Your output will completely replace the selected code"
|
||||||
"code block markers";
|
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||||
systemPrompt += "\n- The output should be ready to insert directly into the editor";
|
"\n- Your output will be inserted at the cursor location";
|
||||||
systemPrompt += "\n- Follow the existing code style and indentation patterns";
|
|
||||||
|
|
||||||
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
|
systemPrompt += "\n\n## Formatting Rules:"
|
||||||
|
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
||||||
|
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
||||||
|
"\n- Do NOT add comments explaining what you changed"
|
||||||
|
"\n- Do NOT repeat existing code, be precise with context"
|
||||||
|
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
||||||
|
"\n- The output must be ready to insert directly into the editor as-is";
|
||||||
|
|
||||||
|
systemPrompt += "\n\n## Indentation and Whitespace:";
|
||||||
|
|
||||||
|
if (cursor.hasSelection()) {
|
||||||
|
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : startBlock.text()) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
||||||
|
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
||||||
|
"\n- Each line in your output must maintain this base indentation")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
systemPrompt += "\n- PRESERVE all indentation from the original code";
|
||||||
|
} else {
|
||||||
|
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
||||||
|
QString lineText = block.text();
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : lineText) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation"
|
||||||
|
"\n- If generating multiline code, EVERY line must start with at least %1 spaces"
|
||||||
|
"\n- If generating single-line code, it will be inserted inline (no indentation needed)")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
||||||
|
"\n- Maintain consistent indentation for nested blocks"
|
||||||
|
"\n- Do NOT remove or reduce the base indentation level"
|
||||||
|
"\n\n## Code Style:"
|
||||||
|
"\n- Match the coding style of the surrounding code (naming, spacing, braces, etc.)"
|
||||||
|
"\n- Preserve the original code structure when possible"
|
||||||
|
"\n- Only change what is necessary to fulfill the user's request";
|
||||||
|
|
||||||
|
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
|
||||||
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,19 +387,7 @@ void QuickRefactorHandler::handleLLMResponse(
|
|||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
m_isRefactoringInProgress = false;
|
m_isRefactoringInProgress = false;
|
||||||
|
QString cleanedResponse = LLMCore::ResponseCleaner::clean(response);
|
||||||
QString cleanedResponse = response.trimmed();
|
|
||||||
if (cleanedResponse.startsWith("```")) {
|
|
||||||
int firstNewLine = cleanedResponse.indexOf('\n');
|
|
||||||
int lastFence = cleanedResponse.lastIndexOf("```");
|
|
||||||
|
|
||||||
if (firstNewLine != -1 && lastFence > firstNewLine) {
|
|
||||||
cleanedResponse
|
|
||||||
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
|
|
||||||
} else if (lastFence != -1) {
|
|
||||||
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.newText = cleanedResponse;
|
result.newText = cleanedResponse;
|
||||||
|
|||||||
77
README.md
@@ -125,21 +125,59 @@ 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>
|
||||||
|
|
||||||
|
1. **Open QodeAssist Settings**
|
||||||
|
2. **Select a Preset** - Choose from the Quick Setup dropdown:
|
||||||
|
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
|
||||||
|
- **OpenAI** (gpt-5.2-codex)
|
||||||
|
- **Mistral AI** (Codestral 2501)
|
||||||
|
- **Google AI** (Gemini 2.5 Flash)
|
||||||
|
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
||||||
|
|
||||||
|
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
||||||
|
|
||||||
|
### Manual Provider Configuration
|
||||||
|
|
||||||
|
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
|
||||||
|
|
||||||
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
|
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
|
||||||
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
||||||
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
|
- **[Anthropic Claude](docs/claude-configuration.md)** - Cloud provider (manual setup)
|
||||||
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider
|
- **[OpenAI](docs/openai-configuration.md)** - Cloud provider (includes Responses API support)
|
||||||
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
|
- **[Mistral AI](docs/mistral-configuration.md)** - Cloud provider
|
||||||
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
|
- **[Google AI](docs/google-ai-configuration.md)** - Cloud provider
|
||||||
- **LM Studio** - Local LLM provider
|
- **LM Studio** - Local LLM provider
|
||||||
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
|
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
|
||||||
|
|
||||||
|
### Recommended Models for Best Experience
|
||||||
|
|
||||||
|
For optimal coding assistance, we recommend using these top-tier models:
|
||||||
|
|
||||||
|
**Best for Code Completion & Refactoring:**
|
||||||
|
- **Claude 4.5 Haiku or Sonnet** (Anthropic)
|
||||||
|
- **GPT-5.1-codex or codex-mini** (OpenAI Responses API)
|
||||||
|
- **Codestral** (Mistral)
|
||||||
|
|
||||||
|
**Best for Chat Assistant:**
|
||||||
|
- **Claude 4.5 Sonnet** (Anthropic) - Outstanding reasoning and natural conversation flow
|
||||||
|
- **GPT-5.1-codex** (OpenAI Responses API) - Latest model with advanced capabilities
|
||||||
|
- **Gemini 2.5 or 3.0** (Google AI) - Latest models from Google
|
||||||
|
- **Mistral large** (Mistral) - Fast and capable
|
||||||
|
|
||||||
|
**Local models:**
|
||||||
|
- **Qwen3-coder** (Qwen) - Best local models
|
||||||
|
|
||||||
### Additional Configuration
|
### Additional Configuration
|
||||||
|
|
||||||
|
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
||||||
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
||||||
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
|
||||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
||||||
|
|
||||||
@@ -155,14 +193,14 @@ QodeAssist supports multiple LLM providers. Choose your preferred provider and f
|
|||||||
|
|
||||||
QodeAssist offers two trigger modes for code completion:
|
QodeAssist offers two trigger modes for code completion:
|
||||||
|
|
||||||
**Hint-based (Default, Recommended)**
|
**Hint-based**
|
||||||
- Shows a hint indicator near cursor when you type 3+ characters
|
- Shows a hint indicator near cursor when you type 3+ characters
|
||||||
- Press **Space** (or custom key) to request completion
|
- Press **Space** (or custom key) to request completion
|
||||||
- **Best for**: Paid APIs (Claude, OpenAI), conscious control
|
- **Best for**: Paid APIs (Claude, OpenAI), conscious control
|
||||||
- **Benefits**: No unexpected API charges, full control over requests, no workflow interruption
|
- **Benefits**: No unexpected API charges, full control over requests, no workflow interruption
|
||||||
- **Visual**: Clear indicator shows when completion is ready
|
- **Visual**: Clear indicator shows when completion is ready
|
||||||
|
|
||||||
**Automatic**
|
**Automatic(Default)**
|
||||||
- Automatically requests completion after typing threshold
|
- Automatically requests completion after typing threshold
|
||||||
- Works immediately without additional keypresses
|
- Works immediately without additional keypresses
|
||||||
- **Best for**: Local models (Ollama, llama.cpp), maximum automation
|
- **Best for**: Local models (Ollama, llama.cpp), maximum automation
|
||||||
@@ -176,14 +214,14 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
|||||||
- Multiple chat panels: side panel, bottom panel, and popup window
|
- Multiple chat panels: side panel, bottom panel, and popup window
|
||||||
- Chat history with auto-save and restore
|
- Chat history with auto-save and restore
|
||||||
- Token usage monitoring
|
- Token usage monitoring
|
||||||
|
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
|
||||||
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
||||||
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
||||||
- Automatic syncing with open editor files (optional)
|
- Automatic syncing with open editor files (optional)
|
||||||
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
||||||
|
|
||||||
### Quick Refactoring
|
### Quick Refactoring
|
||||||
- Inline code refactoring directly in the editor with AI assistance
|
- Inline code refactoring directly in the editor with AI assistance
|
||||||
- Selection-based improvements with instant code replacement
|
|
||||||
- Built-in quick actions (repeat, improve, alternative)
|
|
||||||
- **Custom instructions library** with search and autocomplete
|
- **Custom instructions library** with search and autocomplete
|
||||||
- Create, edit, and manage reusable refactoring templates
|
- Create, edit, and manage reusable refactoring templates
|
||||||
- Combine base instructions with specific details
|
- Combine base instructions with specific details
|
||||||
@@ -262,22 +300,24 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
│ CHAT ASSISTANT │
|
│ CHAT ASSISTANT │
|
||||||
├─────────────────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
│ 1. System Prompt (from Chat Assistant Settings) │
|
│ 1. System Prompt (from Chat Assistant Settings) │
|
||||||
│ 2. Project Rules: │
|
│ 2. Agent Role (optional, from role selector): │
|
||||||
|
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
|
||||||
|
│ 3. Project Rules: │
|
||||||
│ ├─ .qodeassist/rules/common/*.md │
|
│ ├─ .qodeassist/rules/common/*.md │
|
||||||
│ └─ .qodeassist/rules/chat/*.md │
|
│ └─ .qodeassist/rules/chat/*.md │
|
||||||
│ 3. File Context (optional): │
|
│ 4. File Context (optional): │
|
||||||
│ ├─ Attached files (manual) │
|
│ ├─ Attached files (manual) │
|
||||||
│ ├─ Linked files (persistent) │
|
│ ├─ Linked files (persistent) │
|
||||||
│ └─ Open editor files (if auto-sync enabled) │
|
│ └─ Open editor files (if auto-sync enabled) │
|
||||||
│ 4. Tool Definitions (if enabled): │
|
│ 5. Tool Definitions (if enabled): │
|
||||||
│ ├─ ReadProjectFileByName │
|
│ ├─ ReadProjectFileByName │
|
||||||
│ ├─ ListProjectFiles │
|
│ ├─ ListProjectFiles │
|
||||||
│ ├─ SearchInProject │
|
│ ├─ SearchInProject │
|
||||||
│ └─ GetIssuesList │
|
│ └─ GetIssuesList │
|
||||||
│ 5. Conversation History │
|
│ 6. Conversation History │
|
||||||
│ 6. User Message │
|
│ 7. User Message │
|
||||||
│ │
|
│ │
|
||||||
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │
|
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
|
||||||
│ [History: Previous messages] │
|
│ [History: Previous messages] │
|
||||||
│ [User: FileContext + UserMessage] │
|
│ [User: FileContext + UserMessage] │
|
||||||
└─────────────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
@@ -323,6 +363,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
|
|||||||
|
|
||||||
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
||||||
- **System Prompts** are configured independently for each feature in Settings
|
- **System Prompts** are configured independently for each feature in Settings
|
||||||
|
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
|
||||||
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
- **FIM vs Non-FIM models** for code completion use different System Prompts:
|
||||||
- FIM models: Direct completion prompt
|
- FIM models: Direct completion prompt
|
||||||
- Non-FIM models: Prompt includes response formatting instructions
|
- Non-FIM models: Prompt includes response formatting instructions
|
||||||
@@ -330,14 +371,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 |
|
||||||
|
|||||||
115
RefactorContextHelper.hpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
struct RefactorContext
|
||||||
|
{
|
||||||
|
QString originalText;
|
||||||
|
QString textBeforeCursor;
|
||||||
|
QString textAfterCursor;
|
||||||
|
QString contextBefore;
|
||||||
|
QString contextAfter;
|
||||||
|
int startPos{0};
|
||||||
|
int endPos{0};
|
||||||
|
bool isInsertion{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class RefactorContextHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static RefactorContext extractContext(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
int contextLinesBefore = 3,
|
||||||
|
int contextLinesAfter = 3)
|
||||||
|
{
|
||||||
|
RefactorContext ctx;
|
||||||
|
if (!editor) {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocument *doc = editor->document();
|
||||||
|
ctx.startPos = range.begin.toPositionInDocument(doc);
|
||||||
|
ctx.endPos = range.end.toPositionInDocument(doc);
|
||||||
|
ctx.isInsertion = (ctx.startPos == ctx.endPos);
|
||||||
|
|
||||||
|
if (!ctx.isInsertion) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.setPosition(ctx.endPos, QTextCursor::KeepAnchor);
|
||||||
|
ctx.originalText = cursor.selectedText();
|
||||||
|
ctx.originalText.replace(QChar(0x2029), "\n");
|
||||||
|
} else {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
|
||||||
|
int posInBlock = cursor.positionInBlock();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, posInBlock);
|
||||||
|
ctx.textBeforeCursor = cursor.selectedText();
|
||||||
|
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
ctx.textAfterCursor = cursor.selectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.contextBefore = extractContextLines(doc, ctx.startPos, contextLinesBefore, true);
|
||||||
|
ctx.contextAfter = extractContextLines(doc, ctx.endPos, contextLinesAfter, false);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString extractContextLines(QTextDocument *doc, int position, int lineCount, bool before)
|
||||||
|
{
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
QTextBlock currentBlock = cursor.block();
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
QTextBlock block = currentBlock.previous();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.prepend(block.text());
|
||||||
|
block = block.previous();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QTextBlock block = currentBlock.next();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.append(block.text());
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ qt_add_qml_module(QodeAssistUIControls
|
|||||||
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
|
||||||
|
|||||||
@@ -24,13 +24,32 @@ import QtQuick.Controls.Basic as Basic
|
|||||||
Basic.ComboBox {
|
Basic.ComboBox {
|
||||||
id: control
|
id: control
|
||||||
|
|
||||||
|
property real popupContentWidth: 100
|
||||||
|
|
||||||
implicitWidth: Math.min(contentItem.implicitWidth + 8, 300)
|
implicitWidth: Math.min(contentItem.implicitWidth + 8, 300)
|
||||||
implicitHeight: 30
|
implicitHeight: 30
|
||||||
|
|
||||||
|
function updatePopupWidth() {
|
||||||
|
var maxWidth = 100;
|
||||||
|
if (model) {
|
||||||
|
for (var i = 0; i < model.length; i++) {
|
||||||
|
textMetrics.text = model[i];
|
||||||
|
maxWidth = Math.max(maxWidth, textMetrics.width + 40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupContentWidth = Math.min(maxWidth, 350);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModelChanged: updatePopupWidth()
|
||||||
|
Component.onCompleted: updatePopupWidth()
|
||||||
|
clip: true
|
||||||
|
|
||||||
indicator: Image {
|
indicator: Image {
|
||||||
id: dropdownIcon
|
id: dropdownIcon
|
||||||
|
|
||||||
x: control.width - width - 10
|
x: control.width - width - 10
|
||||||
y: control.topPadding + (control.availableHeight - height) / 2
|
y: control.topPadding + (control.availableHeight - height) / 2
|
||||||
|
|
||||||
width: 12
|
width: 12
|
||||||
height: 8
|
height: 8
|
||||||
source: palette.window.hslLightness > 0.5
|
source: palette.window.hslLightness > 0.5
|
||||||
@@ -92,7 +111,7 @@ Basic.ComboBox {
|
|||||||
|
|
||||||
popup: Popup {
|
popup: Popup {
|
||||||
y: control.height + 2
|
y: control.height + 2
|
||||||
width: control.width
|
width: Math.max(control.width, control.popupContentWidth)
|
||||||
implicitHeight: Math.min(contentItem.implicitHeight, 300)
|
implicitHeight: Math.min(contentItem.implicitHeight, 300)
|
||||||
padding: 4
|
padding: 4
|
||||||
|
|
||||||
@@ -101,6 +120,8 @@ Basic.ComboBox {
|
|||||||
implicitHeight: contentHeight
|
implicitHeight: contentHeight
|
||||||
model: control.popup.visible ? control.delegateModel : null
|
model: control.popup.visible ? control.delegateModel : null
|
||||||
currentIndex: control.highlightedIndex
|
currentIndex: control.highlightedIndex
|
||||||
|
boundsBehavior: ListView.StopAtBounds
|
||||||
|
highlightMoveDuration: 0
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
policy: ScrollBar.AsNeeded
|
policy: ScrollBar.AsNeeded
|
||||||
@@ -124,7 +145,7 @@ Basic.ComboBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
delegate: ItemDelegate {
|
||||||
width: control.width - 8
|
width: control.popup.width - 8
|
||||||
height: 32
|
height: 32
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
@@ -153,5 +174,10 @@ Basic.ComboBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: textMetrics
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
UIControls/qml/QoASeparator.qml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
height: 15
|
||||||
|
width: 1
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
84
UIControls/qml/QoAToolTip.qml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,11 +43,17 @@ ContextManager::ContextManager(QObject *parent)
|
|||||||
QString ContextManager::readFile(const QString &filePath) const
|
QString ContextManager::readFile(const QString &filePath) const
|
||||||
{
|
{
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open file for reading: %1 - %2")
|
||||||
|
.arg(filePath, file.errorString()));
|
||||||
return QString();
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
QTextStream in(&file);
|
QTextStream in(&file);
|
||||||
return in.readAll();
|
QString content = in.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
|
QList<ContentFile> ContextManager::getContentFiles(const QStringList &filePaths) const
|
||||||
|
|||||||
@@ -70,4 +70,18 @@ QString ProjectUtils::findFileInProject(const QString &filename)
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ProjectUtils::getProjectRoot()
|
||||||
|
{
|
||||||
|
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||||
|
|
||||||
|
if (!projects.isEmpty()) {
|
||||||
|
auto project = projects.first();
|
||||||
|
if (project) {
|
||||||
|
return project->projectDirectory().toFSPathString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ public:
|
|||||||
* @return Absolute file path if found, empty string otherwise
|
* @return Absolute file path if found, empty string otherwise
|
||||||
*/
|
*/
|
||||||
static QString findFileInProject(const QString &filename);
|
static QString findFileInProject(const QString &filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the project root directory
|
||||||
|
*
|
||||||
|
* Returns the root directory of the first open project.
|
||||||
|
* If multiple projects are open, returns the first one.
|
||||||
|
*
|
||||||
|
* @return Absolute path to project root, or empty string if no project is open
|
||||||
|
*/
|
||||||
|
static QString getProjectRoot();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
174
docs/agent-roles.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Agent Roles
|
||||||
|
|
||||||
|
Agent Roles allow you to define different AI personas with specialized system prompts for various tasks. Switch between roles instantly in the chat interface to adapt the AI's behavior to your current needs.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Agent Roles are reusable system prompt configurations that modify how the AI assistant responds. Instead of manually changing system prompts, you can create roles like "Developer", "Code Reviewer", or "Documentation Writer" and switch between them with a single click.
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- **Quick Switching**: Change roles from the chat toolbar dropdown
|
||||||
|
- **Custom Prompts**: Each role has its own specialized system prompt
|
||||||
|
- **Built-in Roles**: Pre-configured Developer and Code Reviewer roles
|
||||||
|
- **Persistent**: Roles are saved locally and loaded on startup
|
||||||
|
- **Extensible**: Create unlimited custom roles for different tasks
|
||||||
|
|
||||||
|
## Default Roles
|
||||||
|
|
||||||
|
QodeAssist comes with three built-in roles:
|
||||||
|
|
||||||
|
### Developer
|
||||||
|
Experienced Qt/C++ developer with a structured workflow: analyze the problem, propose a solution, wait for approval, then implement. Best for implementation tasks where you want thoughtful, minimal code changes.
|
||||||
|
|
||||||
|
### Code Reviewer
|
||||||
|
Expert C++/QML code reviewer specializing in C++20 and Qt6. Checks for bugs, memory leaks, thread safety, Qt patterns, and production readiness. Provides direct, specific feedback with code examples.
|
||||||
|
|
||||||
|
### Researcher
|
||||||
|
Research-oriented developer who investigates problems and explores solutions. Analyzes problems, presents multiple approaches with trade-offs, and recommends the best option. Does not write implementation code — focuses on helping you make informed decisions.
|
||||||
|
|
||||||
|
## Using Agent Roles
|
||||||
|
|
||||||
|
### Switching Roles in Chat
|
||||||
|
|
||||||
|
1. Open the Chat Assistant (side panel, bottom panel, or popup window)
|
||||||
|
2. Locate the **Role selector** dropdown in the top toolbar (next to the configuration selector)
|
||||||
|
3. Select a role from the dropdown
|
||||||
|
4. The AI will now use the selected role's system prompt
|
||||||
|
|
||||||
|
**Note**: Selecting "No Role" uses only the base system prompt without role specialization.
|
||||||
|
|
||||||
|
### Viewing Active Role
|
||||||
|
|
||||||
|
Click the **Context** button (📋) in the chat toolbar to view:
|
||||||
|
- Base system prompt
|
||||||
|
- Current agent role and its system prompt
|
||||||
|
- Active project rules
|
||||||
|
|
||||||
|
## Managing Agent Roles
|
||||||
|
|
||||||
|
### Opening the Role Manager
|
||||||
|
|
||||||
|
Navigate to: `Qt Creator → Preferences → QodeAssist → Chat Assistant`
|
||||||
|
|
||||||
|
Scroll down to the **Agent Roles** section where you can manage all your roles.
|
||||||
|
|
||||||
|
### Creating a New Role
|
||||||
|
|
||||||
|
1. Click **Add...** button
|
||||||
|
2. Fill in the role details:
|
||||||
|
- **Name**: Display name shown in the dropdown (e.g., "Documentation Writer")
|
||||||
|
- **ID**: Unique identifier for the role file (e.g., "doc_writer")
|
||||||
|
- **Description**: Brief explanation of the role's purpose
|
||||||
|
- **System Prompt**: The specialized instructions for this role
|
||||||
|
3. Click **OK** to save
|
||||||
|
|
||||||
|
### Editing a Role
|
||||||
|
|
||||||
|
1. Select a role from the list
|
||||||
|
2. Click **Edit...** or double-click the role
|
||||||
|
3. Modify the fields as needed
|
||||||
|
4. Click **OK** to save changes
|
||||||
|
|
||||||
|
**Note**: Built-in roles cannot be edited directly. Duplicate them to create a modifiable copy.
|
||||||
|
|
||||||
|
### Duplicating a Role
|
||||||
|
|
||||||
|
1. Select a role to duplicate
|
||||||
|
2. Click **Duplicate...**
|
||||||
|
3. Modify the copy as needed
|
||||||
|
4. Click **OK** to save as a new role
|
||||||
|
|
||||||
|
### Deleting a Role
|
||||||
|
|
||||||
|
1. Select a custom role (built-in roles cannot be deleted)
|
||||||
|
2. Click **Delete**
|
||||||
|
3. Confirm deletion
|
||||||
|
|
||||||
|
## Creating Effective Roles
|
||||||
|
|
||||||
|
### System Prompt Tips
|
||||||
|
|
||||||
|
- **Be specific**: Clearly define the role's expertise and focus areas
|
||||||
|
- **Set expectations**: Describe the desired response format and style
|
||||||
|
- **Include guidelines**: Add specific rules or constraints for responses
|
||||||
|
- **Use structured prompts**: Break down complex roles into bullet points
|
||||||
|
|
||||||
|
## Storage Location
|
||||||
|
|
||||||
|
Agent roles are stored as JSON files in:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.config/QtProject/qtcreator/qodeassist/agent_roles/
|
||||||
|
```
|
||||||
|
|
||||||
|
**On different platforms:**
|
||||||
|
- **Linux**: `~/.config/QtProject/qtcreator/qodeassist/agent_roles/`
|
||||||
|
- **macOS**: `~/Library/Application Support/QtProject/Qt Creator/qodeassist/agent_roles/`
|
||||||
|
- **Windows**: `%APPDATA%\QtProject\qtcreator\qodeassist\agent_roles\`
|
||||||
|
|
||||||
|
### File Format
|
||||||
|
|
||||||
|
Each role is stored as a JSON file named `{id}.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "doc_writer",
|
||||||
|
"name": "Documentation Writer",
|
||||||
|
"description": "Technical documentation and code comments",
|
||||||
|
"systemPrompt": "You are a technical documentation specialist...",
|
||||||
|
"isBuiltin": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Editing
|
||||||
|
|
||||||
|
You can:
|
||||||
|
- Edit JSON files directly in any text editor
|
||||||
|
- Copy role files between machines
|
||||||
|
- Share roles with team members
|
||||||
|
- Version control your roles
|
||||||
|
- Click **Open Roles Folder...** to quickly access the directory
|
||||||
|
|
||||||
|
## How Roles Work
|
||||||
|
|
||||||
|
When a role is selected, the final system prompt is composed as:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Final System Prompt = Base Prompt + Role Prompt │
|
||||||
|
├─────────────────────────────────────────────────┤
|
||||||
|
│ 1. Base System Prompt (from Chat Settings) │
|
||||||
|
│ 2. Agent Role System Prompt │
|
||||||
|
│ 3. Project Rules (common/ + chat/) │
|
||||||
|
│ 4. Linked Files Context │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows roles to augment rather than replace your base configuration.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Keep roles focused**: Each role should have a clear, specific purpose
|
||||||
|
2. **Use descriptive names**: Make it easy to identify roles at a glance
|
||||||
|
3. **Test your prompts**: Verify roles produce the expected behavior
|
||||||
|
4. **Iterate and improve**: Refine prompts based on AI responses
|
||||||
|
5. **Share with team**: Export and share useful roles with colleagues
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Role Not Appearing in Dropdown
|
||||||
|
- Restart Qt Creator after adding roles manually
|
||||||
|
- Check JSON file format validity
|
||||||
|
- Verify file is in the correct directory
|
||||||
|
|
||||||
|
### Role Behavior Not as Expected
|
||||||
|
- Review the system prompt for clarity
|
||||||
|
- Check if base system prompt conflicts with role prompt
|
||||||
|
- Try a more specific or detailed prompt
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
|
||||||
|
- [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality
|
||||||
|
- [File Context](file-context.md) - Attaching files to chat context
|
||||||
|
|
||||||
115
docs/chat-summarization.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Chat Summarization
|
||||||
|
|
||||||
|
Chat Summarization allows you to compress long conversations into concise AI-generated summaries. This helps save context tokens and makes it easier to continue work on complex topics without losing important information.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
When conversations grow long, they consume more context tokens with each message. Chat Summarization uses your configured Chat Assistant provider to create an intelligent summary that preserves:
|
||||||
|
|
||||||
|
- Key decisions and conclusions
|
||||||
|
- Technical details and code references
|
||||||
|
- Important context for continuing the conversation
|
||||||
|
|
||||||
|
**Key Features:**
|
||||||
|
- **One-click compression**: Summarize directly from the chat toolbar
|
||||||
|
- **Preserves original**: Creates a new chat file, keeping the original intact
|
||||||
|
- **Smart summaries**: AI extracts the most relevant information
|
||||||
|
- **Markdown formatted**: Summaries are well-structured and readable
|
||||||
|
|
||||||
|
## Using Chat Summarization
|
||||||
|
|
||||||
|
### Compressing a Chat
|
||||||
|
|
||||||
|
1. Open any chat with conversation history
|
||||||
|
2. Click the **Compress** button (📦) in the chat top bar
|
||||||
|
3. Wait for the AI to generate the summary
|
||||||
|
4. A new chat opens with the compressed summary
|
||||||
|
|
||||||
|
### What Gets Preserved
|
||||||
|
|
||||||
|
The summarization process:
|
||||||
|
- Maintains chronological flow of the discussion
|
||||||
|
- Keeps technical details, code snippets, and file references
|
||||||
|
- Preserves key decisions and conclusions
|
||||||
|
- Aims for 30-40% of the original conversation length
|
||||||
|
|
||||||
|
### What Gets Filtered
|
||||||
|
|
||||||
|
The following message types are excluded from summarization:
|
||||||
|
- Tool call results (file reads, searches)
|
||||||
|
- File edit blocks
|
||||||
|
- Thinking/reasoning blocks
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ CHAT SUMMARIZATION │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. Original chat messages are collected │
|
||||||
|
│ 2. Tool/thinking messages are filtered out │
|
||||||
|
│ 3. AI generates a structured summary │
|
||||||
|
│ 4. New chat file is created with summary as first message │
|
||||||
|
│ 5. Original chat remains unchanged │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Naming
|
||||||
|
|
||||||
|
Compressed chats are saved with a unique suffix:
|
||||||
|
```
|
||||||
|
original_chat.json → original_chat_a1b2c.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Both files appear in your chat history, allowing you to switch between the full conversation and the summary.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Summarize at natural breakpoints**: Compress after completing a major task or topic
|
||||||
|
2. **Review the summary**: Ensure important details were captured before continuing
|
||||||
|
3. **Keep originals**: Don't delete original chats until you've verified the summary is sufficient
|
||||||
|
4. **Use for long sessions**: Most beneficial for conversations with 20+ messages
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
**Good candidates for summarization:**
|
||||||
|
- Long debugging sessions with resolved issues
|
||||||
|
- Feature implementation discussions with final decisions
|
||||||
|
- Research conversations where conclusions were reached
|
||||||
|
- Any chat approaching context limits
|
||||||
|
|
||||||
|
**Consider keeping full history for:**
|
||||||
|
- Ongoing work that may need exact message references
|
||||||
|
- Conversations with important code snippets you'll copy
|
||||||
|
- Discussions where the reasoning process matters
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Chat Summarization uses your current Chat Assistant settings:
|
||||||
|
- **Provider**: Same as Chat Assistant (Settings → QodeAssist → General)
|
||||||
|
- **Model**: Same as Chat Assistant
|
||||||
|
- **Template**: Same as Chat Assistant
|
||||||
|
|
||||||
|
No additional configuration is required.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Compression Button Not Visible
|
||||||
|
- Ensure you have an active chat with messages
|
||||||
|
- Check that the chat top bar is visible
|
||||||
|
|
||||||
|
### Compression Fails
|
||||||
|
- Verify your Chat Assistant provider is configured correctly
|
||||||
|
- Check network connectivity
|
||||||
|
- Ensure the model supports chat completions
|
||||||
|
|
||||||
|
### Summary Missing Details
|
||||||
|
- The AI aims for 30-40% compression; some details may be condensed
|
||||||
|
- For critical information, keep the original chat
|
||||||
|
- Consider summarizing smaller conversation segments
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [Agent Roles](agent-roles.md) - Switch between AI personas
|
||||||
|
- [File Context](file-context.md) - Attach files to chat
|
||||||
|
- [Project Rules](project-rules.md) - Customize AI behavior
|
||||||
@@ -28,6 +28,44 @@ ollama run qwen2.5-coder:32b
|
|||||||
|
|
||||||
You're all set! QodeAssist is now ready to use in Qt Creator.
|
You're all set! QodeAssist is now ready to use in Qt Creator.
|
||||||
|
|
||||||
|
## Extended Thinking Mode
|
||||||
|
|
||||||
|
Ollama supports extended thinking mode for models that are capable of deep reasoning (such as DeepSeek-R1, QwQ, and similar reasoning models). This mode allows the model to show its step-by-step reasoning process before providing the final answer.
|
||||||
|
|
||||||
|
### How to Enable
|
||||||
|
|
||||||
|
**For Chat Assistant:**
|
||||||
|
1. Navigate to Qt Creator > Preferences > QodeAssist > Chat Assistant
|
||||||
|
2. In the "Extended Thinking (Claude, Ollama)" section, check "Enable extended thinking mode"
|
||||||
|
3. Select a reasoning-capable model (e.g., deepseek-r1:8b, qwq:32b)
|
||||||
|
4. Click Apply
|
||||||
|
|
||||||
|
**For Quick Refactoring:**
|
||||||
|
1. Navigate to Qt Creator > Preferences > QodeAssist > Quick Refactor
|
||||||
|
2. Check "Enable Thinking Mode"
|
||||||
|
3. Configure thinking budget and max tokens as needed
|
||||||
|
4. Click Apply
|
||||||
|
|
||||||
|
### Supported Models
|
||||||
|
|
||||||
|
Thinking mode works best with models specifically designed for reasoning:
|
||||||
|
- **DeepSeek-R1** series (deepseek-r1:8b, deepseek-r1:14b, deepseek-r1:32b)
|
||||||
|
- **QwQ** series (qwq:32b)
|
||||||
|
- Other models trained for chain-of-thought reasoning
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
When thinking mode is enabled:
|
||||||
|
1. The model generates internal reasoning (visible in the chat as "Thinking" blocks)
|
||||||
|
2. After reasoning, it provides the final answer
|
||||||
|
3. You can collapse/expand thinking blocks to focus on the final answer
|
||||||
|
4. Temperature is automatically set to 1.0 for optimal reasoning performance
|
||||||
|
|
||||||
|
**Technical Details:**
|
||||||
|
- Thinking mode adds the `enable_thinking: true` parameter to requests sent to Ollama
|
||||||
|
- This is natively supported by the Ollama API for compatible models
|
||||||
|
- Works in both Chat Assistant and Quick Refactoring contexts
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Example of Ollama settings: (click to expand)</summary>
|
<summary>Example of Ollama settings: (click to expand)</summary>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
# Configure for OpenAI
|
# Configure for OpenAI
|
||||||
|
|
||||||
|
QodeAssist supports both OpenAI's standard Chat Completions API and the new Responses API, giving you access to the latest GPT models including GPT-5.1 and GPT-5.1-codex.
|
||||||
|
|
||||||
|
## Standard OpenAI Configuration
|
||||||
|
|
||||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||||
2. Go to Provider Settings tab and configure OpenAI api key
|
2. Go to Provider Settings tab and configure OpenAI api key
|
||||||
3. Return to General tab and configure:
|
3. Return to General tab and configure:
|
||||||
- Set "OpenAI" as the provider for code completion or/and chat assistant
|
- Set "OpenAI" as the provider for code completion or/and chat assistant
|
||||||
- Set the OpenAI URL (https://api.openai.com)
|
- Set the OpenAI URL (https://api.openai.com)
|
||||||
- Select your preferred model (e.g., gpt-4o)
|
- Select your preferred model (e.g., gpt-4o, gpt-5.1, gpt-5.1-codex)
|
||||||
- Choose the OpenAI template for code completion or/and chat
|
- Choose the OpenAI template for code completion or/and chat
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -14,3 +18,15 @@
|
|||||||
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
|
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## OpenAI Responses API Configuration
|
||||||
|
|
||||||
|
The Responses API is OpenAI's newer endpoint that provides enhanced capabilities and improved performance. It supports the latest GPT-5.1 models.
|
||||||
|
|
||||||
|
1. Open Qt Creator settings and navigate to the QodeAssist section
|
||||||
|
2. Go to Provider Settings tab and configure OpenAI api key
|
||||||
|
3. Return to General tab and configure:
|
||||||
|
- Set "OpenAI Responses" as the provider for code completion or/and chat assistant
|
||||||
|
- Set the OpenAI URL (https://api.openai.com)
|
||||||
|
- Select your preferred model (e.g., gpt-5.1, gpt-5.1-codex)
|
||||||
|
- Choose the OpenAI Responses template for code completion or/and chat
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Configure:
|
|||||||
- **Advanced Options**: Penalties, context window size
|
- **Advanced Options**: Penalties, context window size
|
||||||
- **Features**: Tool calling, extended thinking mode
|
- **Features**: Tool calling, extended thinking mode
|
||||||
- **System Prompt**: Customize the base prompt for refactoring
|
- **System Prompt**: Customize the base prompt for refactoring
|
||||||
|
- **How quick refactor looks**: Display type and sizes
|
||||||
|
|
||||||
## Using Quick Refactoring
|
## Using Quick Refactoring
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,31 @@ If issues persist, you can reset settings to their default values:
|
|||||||
- API keys are preserved during reset
|
- API keys are preserved during reset
|
||||||
- You will need to re-select your model after reset
|
- You will need to re-select your model after reset
|
||||||
|
|
||||||
|
## Chat History Migration
|
||||||
|
|
||||||
|
### Images not showing in old chats (version 0.5.x → 0.6.x)
|
||||||
|
|
||||||
|
If you have chat histories from QodeAssist version 0.5.x or earlier, images may not display correctly due to a storage structure change.
|
||||||
|
|
||||||
|
**Solution:** Rename the content folder for each affected chat:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to your chat history folder
|
||||||
|
cd ~/path/to/chat_history
|
||||||
|
|
||||||
|
# For each chat file, rename its folder
|
||||||
|
mv chat_name_images chat_name_content
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
mv my_conversation_2024-11-28_images my_conversation_2024-11-28_content
|
||||||
|
```
|
||||||
|
|
||||||
|
**What changed:**
|
||||||
|
- Old format (v0.1): Stored files in `chat_name_images/`
|
||||||
|
- New format (v0.2): Stores all content in `chat_name_content/` (both images and text files)
|
||||||
|
|
||||||
## Common Issues
|
## Common Issues
|
||||||
|
|
||||||
### Plugin doesn't appear after installation
|
### Plugin doesn't appear after installation
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ add_library(LLMCore STATIC
|
|||||||
BaseTool.hpp BaseTool.cpp
|
BaseTool.hpp BaseTool.cpp
|
||||||
ContentBlocks.hpp
|
ContentBlocks.hpp
|
||||||
RulesLoader.hpp RulesLoader.cpp
|
RulesLoader.hpp RulesLoader.cpp
|
||||||
|
ResponseCleaner.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(LLMCore
|
target_link_libraries(LLMCore
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QMutexLocker>
|
#include <QMutexLocker>
|
||||||
#include <QUuid>
|
|
||||||
|
|
||||||
#include <Logger.hpp>
|
#include <Logger.hpp>
|
||||||
|
|
||||||
@@ -30,9 +29,7 @@ namespace QodeAssist::LLMCore {
|
|||||||
HttpClient::HttpClient(QObject *parent)
|
HttpClient::HttpClient(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_manager(new QNetworkAccessManager(this))
|
, m_manager(new QNetworkAccessManager(this))
|
||||||
{
|
{}
|
||||||
connect(this, &HttpClient::sendRequest, this, &HttpClient::onSendRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpClient::~HttpClient()
|
HttpClient::~HttpClient()
|
||||||
{
|
{
|
||||||
@@ -44,156 +41,96 @@ HttpClient::~HttpClient()
|
|||||||
m_activeRequests.clear();
|
m_activeRequests.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void HttpClient::onSendRequest(const HttpRequest &request)
|
QFuture<QByteArray> HttpClient::get(const QNetworkRequest &request)
|
||||||
{
|
{
|
||||||
QJsonDocument doc(request.payload);
|
LOG_MESSAGE(QString("HttpClient: GET %1").arg(request.url().toString()));
|
||||||
LOG_MESSAGE(QString("HttpClient: data: %1").arg(doc.toJson(QJsonDocument::Indented)));
|
|
||||||
|
|
||||||
QNetworkReply *reply
|
auto promise = std::make_shared<QPromise<QByteArray>>();
|
||||||
= m_manager->post(request.networkRequest, doc.toJson(QJsonDocument::Compact));
|
promise->start();
|
||||||
addActiveRequest(reply, request.requestId);
|
|
||||||
|
QNetworkReply *reply = m_manager->get(request);
|
||||||
|
setupNonStreamingReply(reply, promise);
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<QByteArray> HttpClient::post(const QNetworkRequest &request, const QJsonObject &payload)
|
||||||
|
{
|
||||||
|
QJsonDocument doc(payload);
|
||||||
|
LOG_MESSAGE(QString("HttpClient: POST %1, data: %2")
|
||||||
|
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
|
||||||
|
|
||||||
|
auto promise = std::make_shared<QPromise<QByteArray>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact));
|
||||||
|
setupNonStreamingReply(reply, promise);
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<QByteArray> HttpClient::del(const QNetworkRequest &request,
|
||||||
|
std::optional<QJsonObject> payload)
|
||||||
|
{
|
||||||
|
auto promise = std::make_shared<QPromise<QByteArray>>();
|
||||||
|
promise->start();
|
||||||
|
|
||||||
|
QNetworkReply *reply;
|
||||||
|
if (payload) {
|
||||||
|
QJsonDocument doc(*payload);
|
||||||
|
LOG_MESSAGE(QString("HttpClient: DELETE %1, data: %2")
|
||||||
|
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
|
||||||
|
reply = m_manager->sendCustomRequest(request, "DELETE", doc.toJson(QJsonDocument::Compact));
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("HttpClient: DELETE %1").arg(request.url().toString()));
|
||||||
|
reply = m_manager->deleteResource(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupNonStreamingReply(reply, promise);
|
||||||
|
|
||||||
|
return promise->future();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpClient::setupNonStreamingReply(QNetworkReply *reply,
|
||||||
|
std::shared_ptr<QPromise<QByteArray>> promise)
|
||||||
|
{
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply, promise]() {
|
||||||
|
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
QByteArray responseBody = reply->readAll();
|
||||||
|
QNetworkReply::NetworkError networkError = reply->error();
|
||||||
|
QString networkErrorString = reply->errorString();
|
||||||
|
|
||||||
|
reply->disconnect();
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("HttpClient: Non-streaming request - HTTP Status: %1").arg(statusCode));
|
||||||
|
|
||||||
|
bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
|
||||||
|
if (hasError) {
|
||||||
|
QString errorMsg = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
|
||||||
|
LOG_MESSAGE(QString("HttpClient: Non-streaming request - Error: %1").arg(errorMsg));
|
||||||
|
promise->setException(
|
||||||
|
std::make_exception_ptr(std::runtime_error(errorMsg.toStdString())));
|
||||||
|
} else {
|
||||||
|
promise->addResult(responseBody);
|
||||||
|
}
|
||||||
|
promise->finish();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpClient::postStreaming(const QString &requestId, const QNetworkRequest &request,
|
||||||
|
const QJsonObject &payload)
|
||||||
|
{
|
||||||
|
QJsonDocument doc(payload);
|
||||||
|
LOG_MESSAGE(QString("HttpClient: POST streaming %1, data: %2")
|
||||||
|
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact));
|
||||||
|
addActiveRequest(reply, requestId);
|
||||||
|
|
||||||
connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead);
|
connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead);
|
||||||
connect(reply, &QNetworkReply::finished, this, &HttpClient::onFinished);
|
connect(reply, &QNetworkReply::finished, this, &HttpClient::onStreamingFinished);
|
||||||
}
|
|
||||||
|
|
||||||
void HttpClient::onReadyRead()
|
|
||||||
{
|
|
||||||
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
|
||||||
|
|
||||||
if (!reply || reply->isFinished())
|
|
||||||
return;
|
|
||||||
|
|
||||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
||||||
if (statusCode >= 400) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString requestId;
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
bool found = false;
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.value() == reply) {
|
|
||||||
requestId = it.key();
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestId.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
QByteArray data = reply->readAll();
|
|
||||||
if (!data.isEmpty()) {
|
|
||||||
emit dataReceived(requestId, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HttpClient::onFinished()
|
|
||||||
{
|
|
||||||
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
|
||||||
if (!reply)
|
|
||||||
return;
|
|
||||||
|
|
||||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
||||||
QByteArray responseBody = reply->readAll();
|
|
||||||
QNetworkReply::NetworkError networkError = reply->error();
|
|
||||||
QString networkErrorString = reply->errorString();
|
|
||||||
|
|
||||||
reply->disconnect();
|
|
||||||
|
|
||||||
QString requestId;
|
|
||||||
bool hasError = false;
|
|
||||||
QString errorMsg;
|
|
||||||
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
bool found = false;
|
|
||||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
|
||||||
if (it.value() == reply) {
|
|
||||||
requestId = it.key();
|
|
||||||
m_activeRequests.erase(it);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
reply->deleteLater();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
errorMsg = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("HttpClient: Request %1 - HTTP Status: %2").arg(requestId).arg(statusCode));
|
|
||||||
|
|
||||||
if (!responseBody.isEmpty()) {
|
|
||||||
LOG_MESSAGE(QString("HttpClient: Request %1 - Response body (%2 bytes): %3")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(responseBody.size())
|
|
||||||
.arg(QString::fromUtf8(responseBody)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
LOG_MESSAGE(QString("HttpClient: Request %1 - Error: %2").arg(requestId, errorMsg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reply->deleteLater();
|
|
||||||
|
|
||||||
if (!requestId.isEmpty()) {
|
|
||||||
emit requestFinished(requestId, !hasError, errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId)
|
|
||||||
{
|
|
||||||
QMutexLocker locker(&m_mutex);
|
|
||||||
m_activeRequests[requestId] = reply;
|
|
||||||
LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId));
|
|
||||||
return requestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString HttpClient::parseErrorFromResponse(
|
|
||||||
int statusCode, const QByteArray &responseBody, const QString &networkErrorString)
|
|
||||||
{
|
|
||||||
QString errorMsg;
|
|
||||||
|
|
||||||
if (!responseBody.isEmpty()) {
|
|
||||||
QJsonDocument errorDoc = QJsonDocument::fromJson(responseBody);
|
|
||||||
if (!errorDoc.isNull() && errorDoc.isObject()) {
|
|
||||||
QJsonObject errorObj = errorDoc.object();
|
|
||||||
if (errorObj.contains("error")) {
|
|
||||||
QJsonObject error = errorObj["error"].toObject();
|
|
||||||
QString message = error["message"].toString();
|
|
||||||
QString type = error["type"].toString();
|
|
||||||
QString code = error["code"].toString();
|
|
||||||
|
|
||||||
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(message);
|
|
||||||
if (!type.isEmpty())
|
|
||||||
errorMsg += QString(" (type: %1)").arg(type);
|
|
||||||
if (!code.isEmpty())
|
|
||||||
errorMsg += QString(" (code: %1)").arg(code);
|
|
||||||
} else {
|
|
||||||
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(networkErrorString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorMsg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HttpClient::cancelRequest(const QString &requestId)
|
void HttpClient::cancelRequest(const QString &requestId)
|
||||||
@@ -212,4 +149,128 @@ void HttpClient::cancelRequest(const QString &requestId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HttpClient::onReadyRead()
|
||||||
|
{
|
||||||
|
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
if (!reply || reply->isFinished())
|
||||||
|
return;
|
||||||
|
|
||||||
|
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
if (statusCode >= 400)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString requestId = findRequestId(reply);
|
||||||
|
if (requestId.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QByteArray data = reply->readAll();
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
emit dataReceived(requestId, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpClient::onStreamingFinished()
|
||||||
|
{
|
||||||
|
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
if (!reply)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
QByteArray responseBody = reply->readAll();
|
||||||
|
QNetworkReply::NetworkError networkError = reply->error();
|
||||||
|
QString networkErrorString = reply->errorString();
|
||||||
|
|
||||||
|
reply->disconnect();
|
||||||
|
|
||||||
|
QString requestId;
|
||||||
|
std::optional<QString> error;
|
||||||
|
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&m_mutex);
|
||||||
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
|
if (it.value() == reply) {
|
||||||
|
requestId = it.key();
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestId.isEmpty()) {
|
||||||
|
reply->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
|
||||||
|
if (hasError) {
|
||||||
|
error = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("HttpClient: Request %1 - HTTP Status: %2").arg(requestId).arg(statusCode));
|
||||||
|
|
||||||
|
if (!responseBody.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("HttpClient: Request %1 - Response body (%2 bytes): %3")
|
||||||
|
.arg(requestId)
|
||||||
|
.arg(responseBody.size())
|
||||||
|
.arg(QString::fromUtf8(responseBody)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
LOG_MESSAGE(QString("HttpClient: Request %1 - Error: %2").arg(requestId, *error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
if (!requestId.isEmpty()) {
|
||||||
|
emit requestFinished(requestId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString HttpClient::findRequestId(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&m_mutex);
|
||||||
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
|
if (it.value() == reply)
|
||||||
|
return it.key();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId)
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&m_mutex);
|
||||||
|
m_activeRequests[requestId] = reply;
|
||||||
|
LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString HttpClient::parseErrorFromResponse(
|
||||||
|
int statusCode, const QByteArray &responseBody, const QString &networkErrorString)
|
||||||
|
{
|
||||||
|
if (!responseBody.isEmpty()) {
|
||||||
|
QJsonDocument errorDoc = QJsonDocument::fromJson(responseBody);
|
||||||
|
if (!errorDoc.isNull() && errorDoc.isObject()) {
|
||||||
|
QJsonObject errorObj = errorDoc.object();
|
||||||
|
if (errorObj.contains("error")) {
|
||||||
|
QJsonObject error = errorObj["error"].toObject();
|
||||||
|
QString message = error["message"].toString();
|
||||||
|
QString type = error["type"].toString();
|
||||||
|
QString code = error["code"].toString();
|
||||||
|
|
||||||
|
QString errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(message);
|
||||||
|
if (!type.isEmpty())
|
||||||
|
errorMsg += QString(" (type: %1)").arg(type);
|
||||||
|
if (!code.isEmpty())
|
||||||
|
errorMsg += QString(" (code: %1)").arg(code);
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
return QString("HTTP %1: %2")
|
||||||
|
.arg(statusCode)
|
||||||
|
.arg(QString::fromUtf8(responseBody));
|
||||||
|
}
|
||||||
|
return QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody));
|
||||||
|
}
|
||||||
|
return QString("HTTP %1: %2").arg(statusCode).arg(networkErrorString);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::LLMCore
|
} // namespace QodeAssist::LLMCore
|
||||||
|
|||||||
@@ -19,24 +19,19 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QFuture>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QMap>
|
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QUrl>
|
#include <QPromise>
|
||||||
|
|
||||||
namespace QodeAssist::LLMCore {
|
namespace QodeAssist::LLMCore {
|
||||||
|
|
||||||
struct HttpRequest
|
|
||||||
{
|
|
||||||
QNetworkRequest networkRequest;
|
|
||||||
QString requestId;
|
|
||||||
QJsonObject payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
class HttpClient : public QObject
|
class HttpClient : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -45,21 +40,33 @@ public:
|
|||||||
HttpClient(QObject *parent = nullptr);
|
HttpClient(QObject *parent = nullptr);
|
||||||
~HttpClient();
|
~HttpClient();
|
||||||
|
|
||||||
|
// Non-streaming — return QFuture with full response
|
||||||
|
QFuture<QByteArray> get(const QNetworkRequest &request);
|
||||||
|
QFuture<QByteArray> post(const QNetworkRequest &request, const QJsonObject &payload);
|
||||||
|
QFuture<QByteArray> del(const QNetworkRequest &request,
|
||||||
|
std::optional<QJsonObject> payload = std::nullopt);
|
||||||
|
|
||||||
|
// Streaming — signal-based with requestId
|
||||||
|
void postStreaming(const QString &requestId, const QNetworkRequest &request,
|
||||||
|
const QJsonObject &payload);
|
||||||
|
|
||||||
void cancelRequest(const QString &requestId);
|
void cancelRequest(const QString &requestId);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void sendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
|
||||||
void dataReceived(const QString &requestId, const QByteArray &data);
|
void dataReceived(const QString &requestId, const QByteArray &data);
|
||||||
void requestFinished(const QString &requestId, bool success, const QString &error);
|
void requestFinished(const QString &requestId, std::optional<QString> error);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onSendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
|
||||||
void onReadyRead();
|
void onReadyRead();
|
||||||
void onFinished();
|
void onStreamingFinished();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString addActiveRequest(QNetworkReply *reply, const QString &requestId);
|
void setupNonStreamingReply(QNetworkReply *reply, std::shared_ptr<QPromise<QByteArray>> promise);
|
||||||
QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody, const QString &networkErrorString);
|
|
||||||
|
QString findRequestId(QNetworkReply *reply);
|
||||||
|
void addActiveRequest(QNetworkReply *reply, const QString &requestId);
|
||||||
|
QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody,
|
||||||
|
const QString &networkErrorString);
|
||||||
|
|
||||||
QNetworkAccessManager *m_manager;
|
QNetworkAccessManager *m_manager;
|
||||||
QHash<QString, QNetworkReply *> m_activeRequests;
|
QHash<QString, QNetworkReply *> m_activeRequests;
|
||||||
|
|||||||
51
llmcore/IToolsManager.hpp
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "BaseTool.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::LLMCore {
|
||||||
|
|
||||||
|
class IToolsManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IToolsManager() = default;
|
||||||
|
|
||||||
|
virtual void executeToolCall(
|
||||||
|
const QString &requestId,
|
||||||
|
const QString &toolId,
|
||||||
|
const QString &toolName,
|
||||||
|
const QJsonObject &input) = 0;
|
||||||
|
|
||||||
|
virtual QJsonArray getToolsDefinitions(
|
||||||
|
ToolSchemaFormat format,
|
||||||
|
RunToolsFilter filter = RunToolsFilter::ALL) const = 0;
|
||||||
|
|
||||||
|
virtual void cleanupRequest(const QString &requestId) = 0;
|
||||||
|
virtual void setCurrentSessionId(const QString &sessionId) = 0;
|
||||||
|
virtual void clearTodoSession(const QString &sessionId) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::LLMCore
|
||||||
@@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QFuture>
|
||||||
#include <utils/environment.h>
|
#include <utils/environment.h>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
@@ -27,6 +30,7 @@
|
|||||||
#include "ContextData.hpp"
|
#include "ContextData.hpp"
|
||||||
#include "DataBuffers.hpp"
|
#include "DataBuffers.hpp"
|
||||||
#include "HttpClient.hpp"
|
#include "HttpClient.hpp"
|
||||||
|
#include "IToolsManager.hpp"
|
||||||
#include "PromptTemplate.hpp"
|
#include "PromptTemplate.hpp"
|
||||||
#include "RequestType.hpp"
|
#include "RequestType.hpp"
|
||||||
|
|
||||||
@@ -56,7 +60,7 @@ public:
|
|||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled)
|
bool isThinkingEnabled)
|
||||||
= 0;
|
= 0;
|
||||||
virtual QList<QString> getInstalledModels(const QString &url) = 0;
|
virtual QFuture<QList<QString>> getInstalledModels(const QString &url) = 0;
|
||||||
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
|
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
|
||||||
virtual QString apiKey() const = 0;
|
virtual QString apiKey() const = 0;
|
||||||
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
|
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
|
||||||
@@ -71,6 +75,8 @@ public:
|
|||||||
|
|
||||||
virtual void cancelRequest(const RequestID &requestId);
|
virtual void cancelRequest(const RequestID &requestId);
|
||||||
|
|
||||||
|
virtual IToolsManager *toolsManager() const { return nullptr; }
|
||||||
|
|
||||||
HttpClient *httpClient() const;
|
HttpClient *httpClient() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@@ -78,7 +84,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||||
= 0;
|
= 0;
|
||||||
virtual void onRequestFinished(
|
virtual void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
= 0;
|
= 0;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ enum class ProviderID {
|
|||||||
Claude,
|
Claude,
|
||||||
OpenAI,
|
OpenAI,
|
||||||
OpenAICompatible,
|
OpenAICompatible,
|
||||||
|
OpenAIResponses,
|
||||||
MistralAI,
|
MistralAI,
|
||||||
OpenRouter,
|
OpenRouter,
|
||||||
GoogleAI,
|
GoogleAI,
|
||||||
|
|||||||
119
llmcore/ResponseCleaner.hpp
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
namespace QodeAssist::LLMCore {
|
||||||
|
|
||||||
|
class ResponseCleaner
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static QString clean(const QString &response)
|
||||||
|
{
|
||||||
|
QString cleaned = removeCodeBlocks(response);
|
||||||
|
cleaned = trimWhitespace(cleaned);
|
||||||
|
cleaned = removeExplanations(cleaned);
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString removeCodeBlocks(const QString &text)
|
||||||
|
{
|
||||||
|
if (!text.contains("```")) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegularExpression codeBlockRegex("```\\w*\\n([\\s\\S]*?)```");
|
||||||
|
QRegularExpressionMatch match = codeBlockRegex.match(text);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
return match.captured(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int firstFence = text.indexOf("```");
|
||||||
|
int lastFence = text.lastIndexOf("```");
|
||||||
|
if (firstFence != -1 && lastFence > firstFence) {
|
||||||
|
int firstNewLine = text.indexOf('\n', firstFence);
|
||||||
|
if (firstNewLine != -1) {
|
||||||
|
return text.mid(firstNewLine + 1, lastFence - firstNewLine - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString trimWhitespace(const QString &text)
|
||||||
|
{
|
||||||
|
QString result = text;
|
||||||
|
while (result.startsWith('\n') || result.startsWith('\r')) {
|
||||||
|
result = result.mid(1);
|
||||||
|
}
|
||||||
|
while (result.endsWith('\n') || result.endsWith('\r')) {
|
||||||
|
result.chop(1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString removeExplanations(const QString &text)
|
||||||
|
{
|
||||||
|
static const QStringList explanationPrefixes = {
|
||||||
|
"here's the", "here is the", "here's", "here is",
|
||||||
|
"the refactored", "refactored code:", "code:",
|
||||||
|
"i've refactored", "i refactored", "i've changed", "i changed"
|
||||||
|
};
|
||||||
|
|
||||||
|
QStringList lines = text.split('\n');
|
||||||
|
int startLine = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < qMin(3, lines.size()); ++i) {
|
||||||
|
QString line = lines[i].trimmed().toLower();
|
||||||
|
bool isExplanation = false;
|
||||||
|
|
||||||
|
for (const QString &prefix : explanationPrefixes) {
|
||||||
|
if (line.startsWith(prefix) || line.contains(prefix + " code")) {
|
||||||
|
isExplanation = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.length() < 50 && line.endsWith(':')) {
|
||||||
|
isExplanation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExplanation) {
|
||||||
|
startLine = i + 1;
|
||||||
|
} else if (!line.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startLine > 0 && startLine < lines.size()) {
|
||||||
|
lines = lines.mid(startLine);
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::LLMCore
|
||||||
|
|
||||||
@@ -19,11 +19,9 @@
|
|||||||
|
|
||||||
#include "ClaudeProvider.hpp"
|
#include "ClaudeProvider.hpp"
|
||||||
|
|
||||||
#include <QEventLoop>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
|
||||||
#include "llmcore/ValidationUtils.hpp"
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
@@ -142,11 +140,8 @@ void ClaudeProvider::prepareRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
|
QFuture<QList<QString>> ClaudeProvider::getInstalledModels(const QString &baseUrl)
|
||||||
{
|
{
|
||||||
QList<QString> models;
|
|
||||||
QNetworkAccessManager manager;
|
|
||||||
|
|
||||||
QUrl url(baseUrl + "/v1/models");
|
QUrl url(baseUrl + "/v1/models");
|
||||||
QUrlQuery query;
|
QUrlQuery query;
|
||||||
query.addQueryItem("limit", "1000");
|
query.addQueryItem("limit", "1000");
|
||||||
@@ -160,32 +155,24 @@ QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
|
|||||||
request.setRawHeader("x-api-key", apiKey().toUtf8());
|
request.setRawHeader("x-api-key", apiKey().toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = manager.get(request);
|
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||||
QEventLoop loop;
|
QList<QString> models;
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
|
||||||
QByteArray responseData = reply->readAll();
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
|
||||||
QJsonObject jsonObject = jsonResponse.object();
|
|
||||||
|
|
||||||
if (jsonObject.contains("data")) {
|
if (jsonObject.contains("data")) {
|
||||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||||
for (const QJsonValue &value : modelArray) {
|
for (const QJsonValue &value : modelArray) {
|
||||||
QJsonObject modelObject = value.toObject();
|
QJsonObject modelObject = value.toObject();
|
||||||
if (modelObject.contains("id")) {
|
if (modelObject.contains("id")) {
|
||||||
QString modelId = modelObject["id"].toString();
|
models.append(modelObject["id"].toString());
|
||||||
models.append(modelId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return models;
|
||||||
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(reply->errorString()));
|
}).onFailed([](const std::exception &e) {
|
||||||
}
|
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(e.what()));
|
||||||
|
return QList<QString>{};
|
||||||
reply->deleteLater();
|
});
|
||||||
return models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
@@ -240,12 +227,9 @@ void ClaudeProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("ClaudeProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
LOG_MESSAGE(QString("ClaudeProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ClaudeProvider::supportsTools() const
|
bool ClaudeProvider::supportsTools() const
|
||||||
@@ -268,6 +252,11 @@ void ClaudeProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
|||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LLMCore::IToolsManager *ClaudeProvider::toolsManager() const
|
||||||
|
{
|
||||||
|
return m_toolsManager;
|
||||||
|
}
|
||||||
|
|
||||||
void ClaudeProvider::onDataReceived(
|
void ClaudeProvider::onDataReceived(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||||
{
|
{
|
||||||
@@ -284,11 +273,11 @@ void ClaudeProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ClaudeProvider::onRequestFinished(
|
void ClaudeProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -531,6 +520,7 @@ void ClaudeProvider::handleMessageComplete(const QString &requestId)
|
|||||||
for (auto toolContent : toolUseContent) {
|
for (auto toolContent : toolUseContent) {
|
||||||
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name());
|
||||||
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||||
|
|
||||||
m_toolsManager->executeToolCall(
|
m_toolsManager->executeToolCall(
|
||||||
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -58,13 +58,14 @@ public:
|
|||||||
bool supportImage() const override;
|
bool supportImage() const override;
|
||||||
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
||||||
|
|
||||||
|
LLMCore::IToolsManager *toolsManager() const override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void onDataReceived(
|
void onDataReceived(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
|
|
||||||
#include "GoogleAIProvider.hpp"
|
#include "GoogleAIProvider.hpp"
|
||||||
|
|
||||||
#include <QEventLoop>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QtCore/qurlquery.h>
|
#include <QtCore/qurlquery.h>
|
||||||
|
|
||||||
#include "llmcore/ValidationUtils.hpp"
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
@@ -156,29 +154,17 @@ void GoogleAIProvider::prepareRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> GoogleAIProvider::getInstalledModels(const QString &url)
|
QFuture<QList<QString>> GoogleAIProvider::getInstalledModels(const QString &url)
|
||||||
{
|
{
|
||||||
QList<QString> models;
|
|
||||||
|
|
||||||
QNetworkAccessManager manager;
|
|
||||||
QNetworkRequest request(QString("%1/models?key=%2").arg(url, apiKey()));
|
QNetworkRequest request(QString("%1/models?key=%2").arg(url, apiKey()));
|
||||||
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
QNetworkReply *reply = manager.get(request);
|
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||||
QEventLoop loop;
|
QList<QString> models;
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
|
||||||
QByteArray responseData = reply->readAll();
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
|
||||||
QJsonObject jsonObject = jsonResponse.object();
|
|
||||||
|
|
||||||
if (jsonObject.contains("models")) {
|
if (jsonObject.contains("models")) {
|
||||||
QJsonArray modelArray = jsonObject["models"].toArray();
|
QJsonArray modelArray = jsonObject["models"].toArray();
|
||||||
models.clear();
|
|
||||||
|
|
||||||
for (const QJsonValue &value : modelArray) {
|
for (const QJsonValue &value : modelArray) {
|
||||||
QJsonObject modelObject = value.toObject();
|
QJsonObject modelObject = value.toObject();
|
||||||
if (modelObject.contains("name")) {
|
if (modelObject.contains("name")) {
|
||||||
@@ -190,12 +176,11 @@ QList<QString> GoogleAIProvider::getInstalledModels(const QString &url)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return models;
|
||||||
LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(reply->errorString()));
|
}).onFailed([](const std::exception &e) {
|
||||||
}
|
LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(e.what()));
|
||||||
|
return QList<QString>{};
|
||||||
reply->deleteLater();
|
});
|
||||||
return models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> GoogleAIProvider::validateRequest(
|
QList<QString> GoogleAIProvider::validateRequest(
|
||||||
@@ -254,13 +239,10 @@ void GoogleAIProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
QString("GoogleAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
QString("GoogleAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GoogleAIProvider::supportsTools() const
|
bool GoogleAIProvider::supportsTools() const
|
||||||
@@ -327,11 +309,11 @@ void GoogleAIProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GoogleAIProvider::onRequestFinished(
|
void GoogleAIProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -62,8 +62,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
|
|||||||
@@ -27,11 +27,9 @@
|
|||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProviderSettings.hpp"
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
|
||||||
#include <QEventLoop>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
@@ -71,35 +69,24 @@ bool LMStudioProvider::supportsModelListing() const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> LMStudioProvider::getInstalledModels(const QString &url)
|
QFuture<QList<QString>> LMStudioProvider::getInstalledModels(const QString &url)
|
||||||
{
|
{
|
||||||
QList<QString> models;
|
|
||||||
QNetworkAccessManager manager;
|
|
||||||
QNetworkRequest request(QString("%1%2").arg(url, "/v1/models"));
|
QNetworkRequest request(QString("%1%2").arg(url, "/v1/models"));
|
||||||
|
|
||||||
QNetworkReply *reply = manager.get(request);
|
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||||
|
QList<QString> models;
|
||||||
|
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||||
|
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||||
|
|
||||||
QEventLoop loop;
|
for (const QJsonValue &value : modelArray) {
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QJsonObject modelObject = value.toObject();
|
||||||
loop.exec();
|
models.append(modelObject["id"].toString());
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
|
||||||
QByteArray responseData = reply->readAll();
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
|
||||||
QJsonObject jsonObject = jsonResponse.object();
|
|
||||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
|
||||||
|
|
||||||
for (const QJsonValue &value : modelArray) {
|
|
||||||
QJsonObject modelObject = value.toObject();
|
|
||||||
QString modelId = modelObject["id"].toString();
|
|
||||||
models.append(modelId);
|
|
||||||
}
|
}
|
||||||
} else {
|
return models;
|
||||||
LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(reply->errorString()));
|
}).onFailed([](const std::exception &e) {
|
||||||
}
|
LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(e.what()));
|
||||||
|
return QList<QString>{};
|
||||||
reply->deleteLater();
|
});
|
||||||
return models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> LMStudioProvider::validateRequest(
|
QList<QString> LMStudioProvider::validateRequest(
|
||||||
@@ -149,13 +136,10 @@ void LMStudioProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
QString("LMStudioProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
QString("LMStudioProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LMStudioProvider::supportsTools() const
|
bool LMStudioProvider::supportsTools() const
|
||||||
@@ -195,11 +179,11 @@ void LMStudioProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LMStudioProvider::onRequestFinished(
|
void LMStudioProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -61,8 +61,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
|
|||||||
@@ -26,11 +26,9 @@
|
|||||||
#include "settings/QuickRefactorSettings.hpp"
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
|
|
||||||
#include <QEventLoop>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
@@ -121,9 +119,9 @@ void LlamaCppProvider::prepareRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> LlamaCppProvider::getInstalledModels(const QString &url)
|
QFuture<QList<QString>> LlamaCppProvider::getInstalledModels(const QString &)
|
||||||
{
|
{
|
||||||
return {};
|
return QtFuture::makeReadyFuture(QList<QString>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> LlamaCppProvider::validateRequest(
|
QList<QString> LlamaCppProvider::validateRequest(
|
||||||
@@ -192,13 +190,10 @@ void LlamaCppProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
QString("LlamaCppProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
QString("LlamaCppProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LlamaCppProvider::supportsTools() const
|
bool LlamaCppProvider::supportsTools() const
|
||||||
@@ -250,11 +245,11 @@ void LlamaCppProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LlamaCppProvider::onRequestFinished(
|
void LlamaCppProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -61,8 +61,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
|
|||||||
@@ -27,11 +27,9 @@
|
|||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProviderSettings.hpp"
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
|
||||||
#include <QEventLoop>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
@@ -71,43 +69,32 @@ bool MistralAIProvider::supportsModelListing() const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> MistralAIProvider::getInstalledModels(const QString &url)
|
QFuture<QList<QString>> MistralAIProvider::getInstalledModels(const QString &url)
|
||||||
{
|
{
|
||||||
QList<QString> models;
|
|
||||||
QNetworkAccessManager manager;
|
|
||||||
QNetworkRequest request(QString("%1/v1/models").arg(url));
|
QNetworkRequest request(QString("%1/v1/models").arg(url));
|
||||||
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
if (!apiKey().isEmpty()) {
|
if (!apiKey().isEmpty()) {
|
||||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = manager.get(request);
|
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||||
QEventLoop loop;
|
QList<QString> models;
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
|
||||||
QByteArray responseData = reply->readAll();
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
|
||||||
QJsonObject jsonObject = jsonResponse.object();
|
|
||||||
|
|
||||||
if (jsonObject.contains("data") && jsonObject["object"].toString() == "list") {
|
if (jsonObject.contains("data") && jsonObject["object"].toString() == "list") {
|
||||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||||
for (const QJsonValue &value : modelArray) {
|
for (const QJsonValue &value : modelArray) {
|
||||||
QJsonObject modelObject = value.toObject();
|
QJsonObject modelObject = value.toObject();
|
||||||
if (modelObject.contains("id")) {
|
if (modelObject.contains("id")) {
|
||||||
QString modelId = modelObject["id"].toString();
|
models.append(modelObject["id"].toString());
|
||||||
models.append(modelId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return models;
|
||||||
LOG_MESSAGE(QString("Error fetching Mistral AI models: %1").arg(reply->errorString()));
|
}).onFailed([](const std::exception &e) {
|
||||||
}
|
LOG_MESSAGE(QString("Error fetching Mistral AI models: %1").arg(e.what()));
|
||||||
|
return QList<QString>{};
|
||||||
reply->deleteLater();
|
});
|
||||||
return models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> MistralAIProvider::validateRequest(
|
QList<QString> MistralAIProvider::validateRequest(
|
||||||
@@ -170,13 +157,10 @@ void MistralAIProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
QString("MistralAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
QString("MistralAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MistralAIProvider::supportsTools() const
|
bool MistralAIProvider::supportsTools() const
|
||||||
@@ -216,11 +200,11 @@ void MistralAIProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MistralAIProvider::onRequestFinished(
|
void MistralAIProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -61,8 +61,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
|
|||||||
@@ -70,6 +70,22 @@ void OllamaMessage::handleToolCall(const QJsonObject &toolCall)
|
|||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
QString("OllamaMessage: Structured tool call detected - name=%1, id=%2").arg(name, toolId));
|
QString("OllamaMessage: Structured tool call detected - name=%1, id=%2").arg(name, toolId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OllamaMessage::handleThinkingDelta(const QString &thinking)
|
||||||
|
{
|
||||||
|
LLMCore::ThinkingContent *thinkingContent = getOrCreateThinkingContent();
|
||||||
|
thinkingContent->appendThinking(thinking);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OllamaMessage::handleThinkingComplete(const QString &signature)
|
||||||
|
{
|
||||||
|
if (m_currentThinkingContent) {
|
||||||
|
m_currentThinkingContent->setSignature(signature);
|
||||||
|
LOG_MESSAGE(QString("OllamaMessage: Set thinking signature, length=%1")
|
||||||
|
.arg(signature.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void OllamaMessage::handleDone(bool done)
|
void OllamaMessage::handleDone(bool done)
|
||||||
{
|
{
|
||||||
m_done = done;
|
m_done = done;
|
||||||
@@ -216,6 +232,7 @@ QJsonObject OllamaMessage::toProviderFormat() const
|
|||||||
|
|
||||||
QString textContent;
|
QString textContent;
|
||||||
QJsonArray toolCalls;
|
QJsonArray toolCalls;
|
||||||
|
QString thinkingContent;
|
||||||
|
|
||||||
for (auto block : m_currentBlocks) {
|
for (auto block : m_currentBlocks) {
|
||||||
if (!block)
|
if (!block)
|
||||||
@@ -228,9 +245,15 @@ QJsonObject OllamaMessage::toProviderFormat() const
|
|||||||
toolCall["type"] = "function";
|
toolCall["type"] = "function";
|
||||||
toolCall["function"] = QJsonObject{{"name", tool->name()}, {"arguments", tool->input()}};
|
toolCall["function"] = QJsonObject{{"name", tool->name()}, {"arguments", tool->input()}};
|
||||||
toolCalls.append(toolCall);
|
toolCalls.append(toolCall);
|
||||||
|
} else if (auto thinking = qobject_cast<LLMCore::ThinkingContent *>(block)) {
|
||||||
|
thinkingContent += thinking->thinking();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!thinkingContent.isEmpty()) {
|
||||||
|
message["thinking"] = thinkingContent;
|
||||||
|
}
|
||||||
|
|
||||||
if (!textContent.isEmpty()) {
|
if (!textContent.isEmpty()) {
|
||||||
message["content"] = textContent;
|
message["content"] = textContent;
|
||||||
}
|
}
|
||||||
@@ -275,6 +298,17 @@ QList<LLMCore::ToolUseContent *> OllamaMessage::getCurrentToolUseContent() const
|
|||||||
return toolBlocks;
|
return toolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<LLMCore::ThinkingContent *> OllamaMessage::getCurrentThinkingContent() const
|
||||||
|
{
|
||||||
|
QList<LLMCore::ThinkingContent *> thinkingBlocks;
|
||||||
|
for (auto block : m_currentBlocks) {
|
||||||
|
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(block)) {
|
||||||
|
thinkingBlocks.append(thinkingContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return thinkingBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
void OllamaMessage::startNewContinuation()
|
void OllamaMessage::startNewContinuation()
|
||||||
{
|
{
|
||||||
LOG_MESSAGE(QString("OllamaMessage: Starting new continuation"));
|
LOG_MESSAGE(QString("OllamaMessage: Starting new continuation"));
|
||||||
@@ -284,6 +318,7 @@ void OllamaMessage::startNewContinuation()
|
|||||||
m_done = false;
|
m_done = false;
|
||||||
m_state = LLMCore::MessageState::Building;
|
m_state = LLMCore::MessageState::Building;
|
||||||
m_contentAddedToTextBlock = false;
|
m_contentAddedToTextBlock = false;
|
||||||
|
m_currentThinkingContent = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OllamaMessage::updateStateFromDone()
|
void OllamaMessage::updateStateFromDone()
|
||||||
@@ -309,4 +344,22 @@ LLMCore::TextContent *OllamaMessage::getOrCreateTextContent()
|
|||||||
return addCurrentContent<LLMCore::TextContent>();
|
return addCurrentContent<LLMCore::TextContent>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LLMCore::ThinkingContent *OllamaMessage::getOrCreateThinkingContent()
|
||||||
|
{
|
||||||
|
if (m_currentThinkingContent) {
|
||||||
|
return m_currentThinkingContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto block : m_currentBlocks) {
|
||||||
|
if (auto thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(block)) {
|
||||||
|
m_currentThinkingContent = thinkingContent;
|
||||||
|
return m_currentThinkingContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentThinkingContent = addCurrentContent<LLMCore::ThinkingContent>();
|
||||||
|
LOG_MESSAGE(QString("OllamaMessage: Created new ThinkingContent block"));
|
||||||
|
return m_currentThinkingContent;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public:
|
|||||||
|
|
||||||
void handleContentDelta(const QString &content);
|
void handleContentDelta(const QString &content);
|
||||||
void handleToolCall(const QJsonObject &toolCall);
|
void handleToolCall(const QJsonObject &toolCall);
|
||||||
|
void handleThinkingDelta(const QString &thinking);
|
||||||
|
void handleThinkingComplete(const QString &signature);
|
||||||
void handleDone(bool done);
|
void handleDone(bool done);
|
||||||
|
|
||||||
QJsonObject toProviderFormat() const;
|
QJsonObject toProviderFormat() const;
|
||||||
@@ -38,6 +40,7 @@ public:
|
|||||||
|
|
||||||
LLMCore::MessageState state() const { return m_state; }
|
LLMCore::MessageState state() const { return m_state; }
|
||||||
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||||
|
QList<LLMCore::ThinkingContent *> getCurrentThinkingContent() const;
|
||||||
QList<LLMCore::ContentBlock *> currentBlocks() const { return m_currentBlocks; }
|
QList<LLMCore::ContentBlock *> currentBlocks() const { return m_currentBlocks; }
|
||||||
|
|
||||||
void startNewContinuation();
|
void startNewContinuation();
|
||||||
@@ -48,11 +51,13 @@ private:
|
|||||||
QList<LLMCore::ContentBlock *> m_currentBlocks;
|
QList<LLMCore::ContentBlock *> m_currentBlocks;
|
||||||
QString m_accumulatedContent;
|
QString m_accumulatedContent;
|
||||||
bool m_contentAddedToTextBlock = false;
|
bool m_contentAddedToTextBlock = false;
|
||||||
|
LLMCore::ThinkingContent *m_currentThinkingContent = nullptr;
|
||||||
|
|
||||||
void updateStateFromDone();
|
void updateStateFromDone();
|
||||||
bool tryParseToolCall();
|
bool tryParseToolCall();
|
||||||
bool isLikelyToolCallJson(const QString &content) const;
|
bool isLikelyToolCallJson(const QString &content) const;
|
||||||
LLMCore::TextContent *getOrCreateTextContent();
|
LLMCore::TextContent *getOrCreateTextContent();
|
||||||
|
LLMCore::ThinkingContent *getOrCreateThinkingContent();
|
||||||
|
|
||||||
template<typename T, typename... Args>
|
template<typename T, typename... Args>
|
||||||
T *addCurrentContent(Args &&...args)
|
T *addCurrentContent(Args &&...args)
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QtCore/qeventloop.h>
|
|
||||||
|
|
||||||
#include "llmcore/ValidationUtils.hpp"
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
@@ -104,12 +102,31 @@ void OllamaProvider::prepareRequest(
|
|||||||
request["keep_alive"] = settings.ollamaLivetime();
|
request["keep_alive"] = settings.ollamaLivetime();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto applyThinkingMode = [&request]() {
|
||||||
|
request["enable_thinking"] = true;
|
||||||
|
QJsonObject options = request["options"].toObject();
|
||||||
|
options["temperature"] = 1.0;
|
||||||
|
request["options"] = options;
|
||||||
|
};
|
||||||
|
|
||||||
if (type == LLMCore::RequestType::CodeCompletion) {
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
applySettings(Settings::codeCompletionSettings());
|
applySettings(Settings::codeCompletionSettings());
|
||||||
} else if (type == LLMCore::RequestType::QuickRefactoring) {
|
} else if (type == LLMCore::RequestType::QuickRefactoring) {
|
||||||
applySettings(Settings::quickRefactorSettings());
|
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||||
|
applySettings(qrSettings);
|
||||||
|
|
||||||
|
if (isThinkingEnabled) {
|
||||||
|
applyThinkingMode();
|
||||||
|
LOG_MESSAGE(QString("OllamaProvider: Thinking mode enabled for QuickRefactoring"));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
applySettings(Settings::chatAssistantSettings());
|
const auto &chatSettings = Settings::chatAssistantSettings();
|
||||||
|
applySettings(chatSettings);
|
||||||
|
|
||||||
|
if (isThinkingEnabled) {
|
||||||
|
applyThinkingMode();
|
||||||
|
LOG_MESSAGE(QString("OllamaProvider: Thinking mode enabled for Chat"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isToolsEnabled) {
|
if (isToolsEnabled) {
|
||||||
@@ -128,35 +145,25 @@ void OllamaProvider::prepareRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OllamaProvider::getInstalledModels(const QString &url)
|
QFuture<QList<QString>> OllamaProvider::getInstalledModels(const QString &url)
|
||||||
{
|
{
|
||||||
QList<QString> models;
|
|
||||||
QNetworkAccessManager manager;
|
|
||||||
QNetworkRequest request(QString("%1%2").arg(url, "/api/tags"));
|
QNetworkRequest request(QString("%1%2").arg(url, "/api/tags"));
|
||||||
prepareNetworkRequest(request);
|
prepareNetworkRequest(request);
|
||||||
QNetworkReply *reply = manager.get(request);
|
|
||||||
|
|
||||||
QEventLoop loop;
|
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QList<QString> models;
|
||||||
loop.exec();
|
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
|
||||||
QByteArray responseData = reply->readAll();
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
|
||||||
QJsonObject jsonObject = jsonResponse.object();
|
|
||||||
QJsonArray modelArray = jsonObject["models"].toArray();
|
QJsonArray modelArray = jsonObject["models"].toArray();
|
||||||
|
|
||||||
for (const QJsonValue &value : modelArray) {
|
for (const QJsonValue &value : modelArray) {
|
||||||
QJsonObject modelObject = value.toObject();
|
QJsonObject modelObject = value.toObject();
|
||||||
QString modelName = modelObject["name"].toString();
|
models.append(modelObject["name"].toString());
|
||||||
models.append(modelName);
|
|
||||||
}
|
}
|
||||||
} else {
|
return models;
|
||||||
LOG_MESSAGE(QString("Error fetching models: %1").arg(reply->errorString()));
|
}).onFailed([](const std::exception &e) {
|
||||||
}
|
LOG_MESSAGE(QString("Error fetching models: %1").arg(e.what()));
|
||||||
|
return QList<QString>{};
|
||||||
reply->deleteLater();
|
});
|
||||||
return models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
@@ -229,12 +236,9 @@ void OllamaProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("OllamaProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
LOG_MESSAGE(QString("OllamaProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OllamaProvider::supportsTools() const
|
bool OllamaProvider::supportsTools() const
|
||||||
@@ -247,6 +251,11 @@ bool OllamaProvider::supportImage() const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OllamaProvider::supportThinking() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void OllamaProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
void OllamaProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||||
{
|
{
|
||||||
LOG_MESSAGE(QString("OllamaProvider: Cancelling request %1").arg(requestId));
|
LOG_MESSAGE(QString("OllamaProvider: Cancelling request %1").arg(requestId));
|
||||||
@@ -288,11 +297,11 @@ void OllamaProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OllamaProvider::onRequestFinished(
|
void OllamaProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -405,12 +414,43 @@ void OllamaProvider::processStreamData(const QString &requestId, const QJsonObje
|
|||||||
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.contains("thinking")) {
|
||||||
|
QString thinkingDelta = data["thinking"].toString();
|
||||||
|
if (!thinkingDelta.isEmpty()) {
|
||||||
|
message->handleThinkingDelta(thinkingDelta);
|
||||||
|
LOG_MESSAGE(QString("OllamaProvider: Received thinking delta, length=%1")
|
||||||
|
.arg(thinkingDelta.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.contains("message")) {
|
if (data.contains("message")) {
|
||||||
QJsonObject messageObj = data["message"].toObject();
|
QJsonObject messageObj = data["message"].toObject();
|
||||||
|
|
||||||
|
if (messageObj.contains("thinking")) {
|
||||||
|
QString thinkingDelta = messageObj["thinking"].toString();
|
||||||
|
if (!thinkingDelta.isEmpty()) {
|
||||||
|
message->handleThinkingDelta(thinkingDelta);
|
||||||
|
|
||||||
|
if (!m_thinkingStarted.contains(requestId)) {
|
||||||
|
auto thinkingBlocks = message->getCurrentThinkingContent();
|
||||||
|
if (!thinkingBlocks.isEmpty() && thinkingBlocks.first()) {
|
||||||
|
QString currentThinking = thinkingBlocks.first()->thinking();
|
||||||
|
QString displayThinking = currentThinking.length() > 50
|
||||||
|
? QString("%1...").arg(currentThinking.left(50))
|
||||||
|
: currentThinking;
|
||||||
|
|
||||||
|
emit thinkingBlockReceived(requestId, displayThinking, "");
|
||||||
|
m_thinkingStarted.insert(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (messageObj.contains("content")) {
|
if (messageObj.contains("content")) {
|
||||||
QString content = messageObj["content"].toString();
|
QString content = messageObj["content"].toString();
|
||||||
if (!content.isEmpty()) {
|
if (!content.isEmpty()) {
|
||||||
|
emitThinkingBlocks(requestId, message);
|
||||||
|
|
||||||
message->handleContentDelta(content);
|
message->handleContentDelta(content);
|
||||||
|
|
||||||
bool hasTextContent = false;
|
bool hasTextContent = false;
|
||||||
@@ -460,6 +500,13 @@ void OllamaProvider::processStreamData(const QString &requestId, const QJsonObje
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data["done"].toBool()) {
|
if (data["done"].toBool()) {
|
||||||
|
if (data.contains("signature")) {
|
||||||
|
QString signature = data["signature"].toString();
|
||||||
|
message->handleThinkingComplete(signature);
|
||||||
|
LOG_MESSAGE(QString("OllamaProvider: Set thinking signature, length=%1")
|
||||||
|
.arg(signature.length()));
|
||||||
|
}
|
||||||
|
|
||||||
message->handleDone(true);
|
message->handleDone(true);
|
||||||
handleMessageComplete(requestId);
|
handleMessageComplete(requestId);
|
||||||
}
|
}
|
||||||
@@ -472,6 +519,8 @@ void OllamaProvider::handleMessageComplete(const QString &requestId)
|
|||||||
|
|
||||||
OllamaMessage *message = m_messages[requestId];
|
OllamaMessage *message = m_messages[requestId];
|
||||||
|
|
||||||
|
emitThinkingBlocks(requestId, message);
|
||||||
|
|
||||||
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||||
LOG_MESSAGE(QString("Ollama message requires tool execution for %1").arg(requestId));
|
LOG_MESSAGE(QString("Ollama message requires tool execution for %1").arg(requestId));
|
||||||
|
|
||||||
@@ -517,6 +566,32 @@ void OllamaProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
|||||||
m_dataBuffers.remove(requestId);
|
m_dataBuffers.remove(requestId);
|
||||||
m_requestUrls.remove(requestId);
|
m_requestUrls.remove(requestId);
|
||||||
m_originalRequests.remove(requestId);
|
m_originalRequests.remove(requestId);
|
||||||
|
m_thinkingEmitted.remove(requestId);
|
||||||
|
m_thinkingStarted.remove(requestId);
|
||||||
m_toolsManager->cleanupRequest(requestId);
|
m_toolsManager->cleanupRequest(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OllamaProvider::emitThinkingBlocks(const QString &requestId, OllamaMessage *message)
|
||||||
|
{
|
||||||
|
if (!message || m_thinkingEmitted.contains(requestId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto thinkingBlocks = message->getCurrentThinkingContent();
|
||||||
|
if (thinkingBlocks.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto thinkingContent : thinkingBlocks) {
|
||||||
|
emit thinkingBlockReceived(
|
||||||
|
requestId, thinkingContent->thinking(), thinkingContent->signature());
|
||||||
|
LOG_MESSAGE(QString("Emitted thinking block for request %1, thinking length=%2, signature "
|
||||||
|
"length=%3")
|
||||||
|
.arg(requestId)
|
||||||
|
.arg(thinkingContent->thinking().length())
|
||||||
|
.arg(thinkingContent->signature().length()));
|
||||||
|
}
|
||||||
|
m_thinkingEmitted.insert(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Providers
|
} // namespace QodeAssist::Providers
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -55,6 +55,7 @@ public:
|
|||||||
|
|
||||||
bool supportsTools() const override;
|
bool supportsTools() const override;
|
||||||
bool supportImage() const override;
|
bool supportImage() const override;
|
||||||
|
bool supportThinking() const override;
|
||||||
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@@ -62,8 +63,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
@@ -73,10 +73,13 @@ private:
|
|||||||
void processStreamData(const QString &requestId, const QJsonObject &data);
|
void processStreamData(const QString &requestId, const QJsonObject &data);
|
||||||
void handleMessageComplete(const QString &requestId);
|
void handleMessageComplete(const QString &requestId);
|
||||||
void cleanupRequest(const LLMCore::RequestID &requestId);
|
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||||
|
void emitThinkingBlocks(const QString &requestId, OllamaMessage *message);
|
||||||
|
|
||||||
QHash<QodeAssist::LLMCore::RequestID, OllamaMessage *> m_messages;
|
QHash<QodeAssist::LLMCore::RequestID, OllamaMessage *> m_messages;
|
||||||
QHash<QodeAssist::LLMCore::RequestID, QUrl> m_requestUrls;
|
QHash<QodeAssist::LLMCore::RequestID, QUrl> m_requestUrls;
|
||||||
QHash<QodeAssist::LLMCore::RequestID, QJsonObject> m_originalRequests;
|
QHash<QodeAssist::LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||||
|
QSet<QString> m_thinkingEmitted;
|
||||||
|
QSet<QString> m_thinkingStarted;
|
||||||
Tools::ToolsManager *m_toolsManager;
|
Tools::ToolsManager *m_toolsManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ void OpenAICompatProvider::prepareRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url)
|
QFuture<QList<QString>> OpenAICompatProvider::getInstalledModels(const QString &)
|
||||||
{
|
{
|
||||||
return QStringList();
|
return QtFuture::makeReadyFuture(QList<QString>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OpenAICompatProvider::validateRequest(
|
QList<QString> OpenAICompatProvider::validateRequest(
|
||||||
@@ -178,13 +178,10 @@ void OpenAICompatProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(
|
LOG_MESSAGE(
|
||||||
QString("OpenAICompatProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
QString("OpenAICompatProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenAICompatProvider::supportsTools() const
|
bool OpenAICompatProvider::supportsTools() const
|
||||||
@@ -224,11 +221,11 @@ void OpenAICompatProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OpenAICompatProvider::onRequestFinished(
|
void OpenAICompatProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -61,8 +61,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
|
|||||||
@@ -27,11 +27,9 @@
|
|||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProviderSettings.hpp"
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
|
||||||
#include <QEventLoop>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QNetworkReply>
|
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
@@ -141,26 +139,17 @@ void OpenAIProvider::prepareRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
|
QFuture<QList<QString>> OpenAIProvider::getInstalledModels(const QString &url)
|
||||||
{
|
{
|
||||||
QList<QString> models;
|
|
||||||
QNetworkAccessManager manager;
|
|
||||||
QNetworkRequest request(QString("%1/v1/models").arg(url));
|
QNetworkRequest request(QString("%1/v1/models").arg(url));
|
||||||
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
if (!apiKey().isEmpty()) {
|
if (!apiKey().isEmpty()) {
|
||||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply *reply = manager.get(request);
|
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||||
QEventLoop loop;
|
QList<QString> models;
|
||||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||||
loop.exec();
|
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
|
||||||
QByteArray responseData = reply->readAll();
|
|
||||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
|
||||||
QJsonObject jsonObject = jsonResponse.object();
|
|
||||||
|
|
||||||
if (jsonObject.contains("data")) {
|
if (jsonObject.contains("data")) {
|
||||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||||
@@ -176,12 +165,11 @@ QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
return models;
|
||||||
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString()));
|
}).onFailed([](const std::exception &e) {
|
||||||
}
|
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what()));
|
||||||
|
return QList<QString>{};
|
||||||
reply->deleteLater();
|
});
|
||||||
return models;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
@@ -235,12 +223,9 @@ void OpenAIProvider::sendRequest(
|
|||||||
QNetworkRequest networkRequest(url);
|
QNetworkRequest networkRequest(url);
|
||||||
prepareNetworkRequest(networkRequest);
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
LLMCore::HttpRequest
|
|
||||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("OpenAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
LOG_MESSAGE(QString("OpenAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||||
|
|
||||||
emit httpClient()->sendRequest(request);
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenAIProvider::supportsTools() const
|
bool OpenAIProvider::supportsTools() const
|
||||||
@@ -280,11 +265,11 @@ void OpenAIProvider::onDataReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OpenAIProvider::onRequestFinished(
|
void OpenAIProvider::onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (error) {
|
||||||
LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, error));
|
LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, *error));
|
||||||
emit requestFailed(requestId, error);
|
emit requestFailed(requestId, *error);
|
||||||
cleanupRequest(requestId);
|
cleanupRequest(requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public:
|
|||||||
LLMCore::RequestType type,
|
LLMCore::RequestType type,
|
||||||
bool isToolsEnabled,
|
bool isToolsEnabled,
|
||||||
bool isThinkingEnabled) override;
|
bool isThinkingEnabled) override;
|
||||||
QList<QString> getInstalledModels(const QString &url) override;
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
QString apiKey() const override;
|
QString apiKey() const override;
|
||||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
@@ -61,8 +61,7 @@ public slots:
|
|||||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
void onRequestFinished(
|
void onRequestFinished(
|
||||||
const QodeAssist::LLMCore::RequestID &requestId,
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
bool success,
|
std::optional<QString> error) override;
|
||||||
const QString &error) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onToolExecutionComplete(
|
void onToolExecutionComplete(
|
||||||
|
|||||||
54
providers/OpenAIResponses/CancelResponseRequest.hpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
struct CancelResponseRequest
|
||||||
|
{
|
||||||
|
QString responseId;
|
||||||
|
|
||||||
|
QString buildUrl(const QString &baseUrl) const
|
||||||
|
{
|
||||||
|
return QString("%1/v1/responses/%2/cancel").arg(baseUrl, responseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const { return !responseId.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class CancelResponseRequestBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CancelResponseRequestBuilder &setResponseId(const QString &id)
|
||||||
|
{
|
||||||
|
m_request.responseId = id;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancelResponseRequest build() const { return m_request; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
CancelResponseRequest m_request;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
69
providers/OpenAIResponses/DeleteResponseRequest.hpp
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
struct DeleteResponseRequest
|
||||||
|
{
|
||||||
|
QString responseId;
|
||||||
|
|
||||||
|
QString buildUrl(const QString &baseUrl) const
|
||||||
|
{
|
||||||
|
return QString("%1/v1/responses/%2").arg(baseUrl, responseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const { return !responseId.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeleteResponseRequestBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeleteResponseRequestBuilder &setResponseId(const QString &id)
|
||||||
|
{
|
||||||
|
m_request.responseId = id;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteResponseRequest build() const { return m_request; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
DeleteResponseRequest m_request;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeleteResponseResult
|
||||||
|
{
|
||||||
|
bool success = false;
|
||||||
|
QString message;
|
||||||
|
|
||||||
|
static DeleteResponseResult fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
DeleteResponseResult result;
|
||||||
|
result.success = obj["success"].toBool();
|
||||||
|
result.message = obj["message"].toString();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
120
providers/OpenAIResponses/GetResponseRequest.hpp
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
struct GetResponseRequest
|
||||||
|
{
|
||||||
|
QString responseId;
|
||||||
|
std::optional<QStringList> include;
|
||||||
|
std::optional<bool> includeObfuscation;
|
||||||
|
std::optional<int> startingAfter;
|
||||||
|
std::optional<bool> stream;
|
||||||
|
|
||||||
|
QString buildUrl(const QString &baseUrl) const
|
||||||
|
{
|
||||||
|
QString url = QString("%1/v1/responses/%2").arg(baseUrl, responseId);
|
||||||
|
QStringList queryParams;
|
||||||
|
|
||||||
|
if (include && !include->isEmpty()) {
|
||||||
|
for (const auto &item : *include) {
|
||||||
|
queryParams.append(QString("include=%1").arg(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeObfuscation) {
|
||||||
|
queryParams.append(
|
||||||
|
QString("include_obfuscation=%1").arg(*includeObfuscation ? "true" : "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startingAfter) {
|
||||||
|
queryParams.append(QString("starting_after=%1").arg(*startingAfter));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream) {
|
||||||
|
queryParams.append(QString("stream=%1").arg(*stream ? "true" : "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!queryParams.isEmpty()) {
|
||||||
|
url += "?" + queryParams.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const { return !responseId.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class GetResponseRequestBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GetResponseRequestBuilder &setResponseId(const QString &id)
|
||||||
|
{
|
||||||
|
m_request.responseId = id;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetResponseRequestBuilder &setInclude(const QStringList &include)
|
||||||
|
{
|
||||||
|
m_request.include = include;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetResponseRequestBuilder &addInclude(const QString &item)
|
||||||
|
{
|
||||||
|
if (!m_request.include) {
|
||||||
|
m_request.include = QStringList();
|
||||||
|
}
|
||||||
|
m_request.include->append(item);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetResponseRequestBuilder &setIncludeObfuscation(bool enabled)
|
||||||
|
{
|
||||||
|
m_request.includeObfuscation = enabled;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetResponseRequestBuilder &setStartingAfter(int sequence)
|
||||||
|
{
|
||||||
|
m_request.startingAfter = sequence;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetResponseRequestBuilder &setStream(bool enabled)
|
||||||
|
{
|
||||||
|
m_request.stream = enabled;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetResponseRequest build() const { return m_request; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
GetResponseRequest m_request;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
219
providers/OpenAIResponses/InputTokensRequest.hpp
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ModelRequest.hpp"
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
struct InputTokensRequest
|
||||||
|
{
|
||||||
|
std::optional<QString> conversation;
|
||||||
|
std::optional<QJsonArray> input;
|
||||||
|
std::optional<QString> instructions;
|
||||||
|
std::optional<QString> model;
|
||||||
|
std::optional<bool> parallelToolCalls;
|
||||||
|
std::optional<QString> previousResponseId;
|
||||||
|
std::optional<QJsonObject> reasoning;
|
||||||
|
std::optional<QJsonObject> text;
|
||||||
|
std::optional<QJsonValue> toolChoice;
|
||||||
|
std::optional<QJsonArray> tools;
|
||||||
|
std::optional<QString> truncation;
|
||||||
|
|
||||||
|
QString buildUrl(const QString &baseUrl) const
|
||||||
|
{
|
||||||
|
return QString("%1/v1/responses/input_tokens").arg(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj;
|
||||||
|
|
||||||
|
if (conversation)
|
||||||
|
obj["conversation"] = *conversation;
|
||||||
|
if (input)
|
||||||
|
obj["input"] = *input;
|
||||||
|
if (instructions)
|
||||||
|
obj["instructions"] = *instructions;
|
||||||
|
if (model)
|
||||||
|
obj["model"] = *model;
|
||||||
|
if (parallelToolCalls)
|
||||||
|
obj["parallel_tool_calls"] = *parallelToolCalls;
|
||||||
|
if (previousResponseId)
|
||||||
|
obj["previous_response_id"] = *previousResponseId;
|
||||||
|
if (reasoning)
|
||||||
|
obj["reasoning"] = *reasoning;
|
||||||
|
if (text)
|
||||||
|
obj["text"] = *text;
|
||||||
|
if (toolChoice)
|
||||||
|
obj["tool_choice"] = *toolChoice;
|
||||||
|
if (tools)
|
||||||
|
obj["tools"] = *tools;
|
||||||
|
if (truncation)
|
||||||
|
obj["truncation"] = *truncation;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const { return input.has_value() || previousResponseId.has_value(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputTokensRequestBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputTokensRequestBuilder &setConversation(const QString &conversationId)
|
||||||
|
{
|
||||||
|
m_request.conversation = conversationId;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setInput(const QJsonArray &input)
|
||||||
|
{
|
||||||
|
m_request.input = input;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &addInputMessage(const Message &message)
|
||||||
|
{
|
||||||
|
if (!m_request.input) {
|
||||||
|
m_request.input = QJsonArray();
|
||||||
|
}
|
||||||
|
m_request.input->append(message.toJson());
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setInstructions(const QString &instructions)
|
||||||
|
{
|
||||||
|
m_request.instructions = instructions;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setModel(const QString &model)
|
||||||
|
{
|
||||||
|
m_request.model = model;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setParallelToolCalls(bool enabled)
|
||||||
|
{
|
||||||
|
m_request.parallelToolCalls = enabled;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setPreviousResponseId(const QString &responseId)
|
||||||
|
{
|
||||||
|
m_request.previousResponseId = responseId;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setReasoning(const QJsonObject &reasoning)
|
||||||
|
{
|
||||||
|
m_request.reasoning = reasoning;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setReasoningEffort(ReasoningEffort effort)
|
||||||
|
{
|
||||||
|
QString effortStr;
|
||||||
|
switch (effort) {
|
||||||
|
case ReasoningEffort::None:
|
||||||
|
effortStr = "none";
|
||||||
|
break;
|
||||||
|
case ReasoningEffort::Minimal:
|
||||||
|
effortStr = "minimal";
|
||||||
|
break;
|
||||||
|
case ReasoningEffort::Low:
|
||||||
|
effortStr = "low";
|
||||||
|
break;
|
||||||
|
case ReasoningEffort::Medium:
|
||||||
|
effortStr = "medium";
|
||||||
|
break;
|
||||||
|
case ReasoningEffort::High:
|
||||||
|
effortStr = "high";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_request.reasoning = QJsonObject{{"effort", effortStr}};
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setText(const QJsonObject &text)
|
||||||
|
{
|
||||||
|
m_request.text = text;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setTextFormat(const TextFormatOptions &format)
|
||||||
|
{
|
||||||
|
m_request.text = format.toJson();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setToolChoice(const QJsonValue &toolChoice)
|
||||||
|
{
|
||||||
|
m_request.toolChoice = toolChoice;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setTools(const QJsonArray &tools)
|
||||||
|
{
|
||||||
|
m_request.tools = tools;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &addTool(const Tool &tool)
|
||||||
|
{
|
||||||
|
if (!m_request.tools) {
|
||||||
|
m_request.tools = QJsonArray();
|
||||||
|
}
|
||||||
|
m_request.tools->append(tool.toJson());
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequestBuilder &setTruncation(const QString &truncation)
|
||||||
|
{
|
||||||
|
m_request.truncation = truncation;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputTokensRequest build() const { return m_request; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
InputTokensRequest m_request;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputTokensResponse
|
||||||
|
{
|
||||||
|
QString object;
|
||||||
|
int inputTokens = 0;
|
||||||
|
|
||||||
|
static InputTokensResponse fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
InputTokensResponse result;
|
||||||
|
result.object = obj["object"].toString();
|
||||||
|
result.inputTokens = obj["input_tokens"].toInt();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
143
providers/OpenAIResponses/ItemTypesReference.hpp
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* REFERENCE: Item Types in List Input Items Response
|
||||||
|
* ===================================================
|
||||||
|
*
|
||||||
|
* The `data` array in ListInputItemsResponse can contain various item types.
|
||||||
|
* This file serves as a reference for all possible item types.
|
||||||
|
*
|
||||||
|
* EXISTING TYPES (already implemented):
|
||||||
|
* -------------------------------------
|
||||||
|
* - MessageOutput (in ResponseObject.hpp)
|
||||||
|
* - FunctionCall (in ResponseObject.hpp)
|
||||||
|
* - ReasoningOutput (in ResponseObject.hpp)
|
||||||
|
* - FileSearchCall (in ResponseObject.hpp)
|
||||||
|
* - CodeInterpreterCall (in ResponseObject.hpp)
|
||||||
|
* - Message (in ModelRequest.hpp) - for input messages
|
||||||
|
*
|
||||||
|
* ADDITIONAL TYPES (to be implemented if needed):
|
||||||
|
* -----------------------------------------------
|
||||||
|
*
|
||||||
|
* 1. Computer Tool Call (computer_call)
|
||||||
|
* - Computer use tool for UI automation
|
||||||
|
* - Properties: action, call_id, id, pending_safety_checks, status, type
|
||||||
|
* - Actions: click, double_click, drag, keypress, move, screenshot, scroll, type, wait
|
||||||
|
*
|
||||||
|
* 2. Computer Tool Call Output (computer_call_output)
|
||||||
|
* - Output from computer tool
|
||||||
|
* - Properties: call_id, id, output, type, acknowledged_safety_checks, status
|
||||||
|
*
|
||||||
|
* 3. Web Search Tool Call (web_search_call)
|
||||||
|
* - Web search results
|
||||||
|
* - Properties: action, id, status, type
|
||||||
|
* - Actions: search, open_page, find
|
||||||
|
*
|
||||||
|
* 4. Image Generation Call (image_generation_call)
|
||||||
|
* - AI image generation request
|
||||||
|
* - Properties: id, result (base64), status, type
|
||||||
|
*
|
||||||
|
* 5. Local Shell Call (local_shell_call)
|
||||||
|
* - Execute shell commands locally
|
||||||
|
* - Properties: action (exec), call_id, id, status, type
|
||||||
|
* - Action properties: command, env, timeout_ms, user, working_directory
|
||||||
|
*
|
||||||
|
* 6. Local Shell Call Output (local_shell_call_output)
|
||||||
|
* - Output from local shell execution
|
||||||
|
* - Properties: id, output (JSON string), type, status
|
||||||
|
*
|
||||||
|
* 7. Shell Tool Call (shell_call)
|
||||||
|
* - Managed shell environment execution
|
||||||
|
* - Properties: action, call_id, id, status, type, created_by
|
||||||
|
*
|
||||||
|
* 8. Shell Call Output (shell_call_output)
|
||||||
|
* - Output from shell tool
|
||||||
|
* - Properties: call_id, id, max_output_length, output (array), type, created_by
|
||||||
|
* - Output chunks: outcome (exit/timeout), stderr, stdout
|
||||||
|
*
|
||||||
|
* 9. Apply Patch Tool Call (apply_patch_call)
|
||||||
|
* - File diff operations
|
||||||
|
* - Properties: call_id, id, operation, status, type, created_by
|
||||||
|
* - Operations: create_file, delete_file, update_file
|
||||||
|
*
|
||||||
|
* 10. Apply Patch Tool Call Output (apply_patch_call_output)
|
||||||
|
* - Output from patch operations
|
||||||
|
* - Properties: call_id, id, status, type, created_by, output
|
||||||
|
*
|
||||||
|
* 11. MCP List Tools (mcp_list_tools)
|
||||||
|
* - List of tools from MCP server
|
||||||
|
* - Properties: id, server_label, tools (array), type, error
|
||||||
|
*
|
||||||
|
* 12. MCP Approval Request (mcp_approval_request)
|
||||||
|
* - Request for human approval
|
||||||
|
* - Properties: arguments, id, name, server_label, type
|
||||||
|
*
|
||||||
|
* 13. MCP Approval Response (mcp_approval_response)
|
||||||
|
* - Response to approval request
|
||||||
|
* - Properties: approval_request_id, approve (bool), id, type, reason
|
||||||
|
*
|
||||||
|
* 14. MCP Tool Call (mcp_call)
|
||||||
|
* - Tool invocation on MCP server
|
||||||
|
* - Properties: arguments, id, name, server_label, type
|
||||||
|
* - Optional: approval_request_id, error, output, status
|
||||||
|
*
|
||||||
|
* 15. Custom Tool Call (custom_tool_call)
|
||||||
|
* - User-defined tool call
|
||||||
|
* - Properties: call_id, input, name, type, id
|
||||||
|
*
|
||||||
|
* 16. Custom Tool Call Output (custom_tool_call_output)
|
||||||
|
* - Output from custom tool
|
||||||
|
* - Properties: call_id, output (string or array), type, id
|
||||||
|
*
|
||||||
|
* 17. Item Reference (item_reference)
|
||||||
|
* - Internal reference to another item
|
||||||
|
* - Properties: id, type
|
||||||
|
*
|
||||||
|
* USAGE:
|
||||||
|
* ------
|
||||||
|
* When parsing ListInputItemsResponse.data array:
|
||||||
|
* 1. Check item["type"] field
|
||||||
|
* 2. Use appropriate parser based on type
|
||||||
|
* 3. For existing types, use ResponseObject.hpp or ModelRequest.hpp
|
||||||
|
* 4. For additional types, implement parsers as needed
|
||||||
|
*
|
||||||
|
* EXAMPLE:
|
||||||
|
* --------
|
||||||
|
* for (const auto &itemValue : response.data) {
|
||||||
|
* const QJsonObject itemObj = itemValue.toObject();
|
||||||
|
* const QString type = itemObj["type"].toString();
|
||||||
|
*
|
||||||
|
* if (type == "message") {
|
||||||
|
* // Use MessageOutput or Message
|
||||||
|
* } else if (type == "function_call") {
|
||||||
|
* // Use FunctionCall
|
||||||
|
* } else if (type == "computer_call") {
|
||||||
|
* // Implement ComputerCall parser
|
||||||
|
* }
|
||||||
|
* // ... handle other types
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
166
providers/OpenAIResponses/ListInputItemsRequest.hpp
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
enum class SortOrder { Ascending, Descending };
|
||||||
|
|
||||||
|
struct ListInputItemsRequest
|
||||||
|
{
|
||||||
|
QString responseId;
|
||||||
|
std::optional<QString> after;
|
||||||
|
std::optional<QStringList> include;
|
||||||
|
std::optional<int> limit;
|
||||||
|
std::optional<SortOrder> order;
|
||||||
|
|
||||||
|
QString buildUrl(const QString &baseUrl) const
|
||||||
|
{
|
||||||
|
QString url = QString("%1/v1/responses/%2/input_items").arg(baseUrl, responseId);
|
||||||
|
QStringList queryParams;
|
||||||
|
|
||||||
|
if (after) {
|
||||||
|
queryParams.append(QString("after=%1").arg(*after));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include && !include->isEmpty()) {
|
||||||
|
for (const auto &item : *include) {
|
||||||
|
queryParams.append(QString("include=%1").arg(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
queryParams.append(QString("limit=%1").arg(*limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order) {
|
||||||
|
QString orderStr = (*order == SortOrder::Ascending) ? "asc" : "desc";
|
||||||
|
queryParams.append(QString("order=%1").arg(orderStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!queryParams.isEmpty()) {
|
||||||
|
url += "?" + queryParams.join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const
|
||||||
|
{
|
||||||
|
if (responseId.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit && (*limit < 1 || *limit > 100)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ListInputItemsRequestBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ListInputItemsRequestBuilder &setResponseId(const QString &id)
|
||||||
|
{
|
||||||
|
m_request.responseId = id;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequestBuilder &setAfter(const QString &itemId)
|
||||||
|
{
|
||||||
|
m_request.after = itemId;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequestBuilder &setInclude(const QStringList &include)
|
||||||
|
{
|
||||||
|
m_request.include = include;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequestBuilder &addInclude(const QString &item)
|
||||||
|
{
|
||||||
|
if (!m_request.include) {
|
||||||
|
m_request.include = QStringList();
|
||||||
|
}
|
||||||
|
m_request.include->append(item);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequestBuilder &setLimit(int limit)
|
||||||
|
{
|
||||||
|
m_request.limit = limit;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequestBuilder &setOrder(SortOrder order)
|
||||||
|
{
|
||||||
|
m_request.order = order;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequestBuilder &setAscendingOrder()
|
||||||
|
{
|
||||||
|
m_request.order = SortOrder::Ascending;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequestBuilder &setDescendingOrder()
|
||||||
|
{
|
||||||
|
m_request.order = SortOrder::Descending;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListInputItemsRequest build() const { return m_request; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
ListInputItemsRequest m_request;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ListInputItemsResponse
|
||||||
|
{
|
||||||
|
QJsonArray data;
|
||||||
|
QString firstId;
|
||||||
|
QString lastId;
|
||||||
|
bool hasMore = false;
|
||||||
|
QString object;
|
||||||
|
|
||||||
|
static ListInputItemsResponse fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
ListInputItemsResponse result;
|
||||||
|
result.data = obj["data"].toArray();
|
||||||
|
result.firstId = obj["first_id"].toString();
|
||||||
|
result.lastId = obj["last_id"].toString();
|
||||||
|
result.hasMore = obj["has_more"].toBool();
|
||||||
|
result.object = obj["object"].toString();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
354
providers/OpenAIResponses/ModelRequest.hpp
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <optional>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
enum class Role { User, Assistant, System, Developer };
|
||||||
|
|
||||||
|
enum class MessageStatus { InProgress, Completed, Incomplete };
|
||||||
|
|
||||||
|
enum class ReasoningEffort { None, Minimal, Low, Medium, High };
|
||||||
|
|
||||||
|
enum class TextFormat { Text, JsonSchema, JsonObject };
|
||||||
|
|
||||||
|
struct InputText
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
return QJsonObject{{"type", "input_text"}, {"text", text}};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !text.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputImage
|
||||||
|
{
|
||||||
|
std::optional<QString> fileId;
|
||||||
|
std::optional<QString> imageUrl;
|
||||||
|
QString detail = "auto";
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj{{"type", "input_image"}, {"detail", detail}};
|
||||||
|
if (fileId)
|
||||||
|
obj["file_id"] = *fileId;
|
||||||
|
if (imageUrl)
|
||||||
|
obj["image_url"] = *imageUrl;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return fileId.has_value() || imageUrl.has_value(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputFile
|
||||||
|
{
|
||||||
|
std::optional<QString> fileId;
|
||||||
|
std::optional<QString> fileUrl;
|
||||||
|
std::optional<QString> fileData;
|
||||||
|
std::optional<QString> filename;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj{{"type", "input_file"}};
|
||||||
|
if (fileId)
|
||||||
|
obj["file_id"] = *fileId;
|
||||||
|
if (fileUrl)
|
||||||
|
obj["file_url"] = *fileUrl;
|
||||||
|
if (fileData)
|
||||||
|
obj["file_data"] = *fileData;
|
||||||
|
if (filename)
|
||||||
|
obj["filename"] = *filename;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return fileId.has_value() || fileUrl.has_value() || fileData.has_value();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MessageContent
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MessageContent(QString text) : m_variant(std::move(text)) {}
|
||||||
|
MessageContent(InputText text) : m_variant(std::move(text)) {}
|
||||||
|
MessageContent(InputImage image) : m_variant(std::move(image)) {}
|
||||||
|
MessageContent(InputFile file) : m_variant(std::move(file)) {}
|
||||||
|
|
||||||
|
QJsonValue toJson() const
|
||||||
|
{
|
||||||
|
return std::visit([](const auto &content) -> QJsonValue {
|
||||||
|
using T = std::decay_t<decltype(content)>;
|
||||||
|
if constexpr (std::is_same_v<T, QString>) {
|
||||||
|
return content;
|
||||||
|
} else {
|
||||||
|
return content.toJson();
|
||||||
|
}
|
||||||
|
}, m_variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return std::visit([](const auto &content) -> bool {
|
||||||
|
using T = std::decay_t<decltype(content)>;
|
||||||
|
if constexpr (std::is_same_v<T, QString>) {
|
||||||
|
return !content.isEmpty();
|
||||||
|
} else {
|
||||||
|
return content.isValid();
|
||||||
|
}
|
||||||
|
}, m_variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::variant<QString, InputText, InputImage, InputFile> m_variant;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Message
|
||||||
|
{
|
||||||
|
Role role;
|
||||||
|
QList<MessageContent> content;
|
||||||
|
std::optional<MessageStatus> status;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj;
|
||||||
|
obj["role"] = roleToString(role);
|
||||||
|
|
||||||
|
if (content.size() == 1) {
|
||||||
|
obj["content"] = content[0].toJson();
|
||||||
|
} else {
|
||||||
|
QJsonArray arr;
|
||||||
|
for (const auto &c : content) {
|
||||||
|
arr.append(c.toJson());
|
||||||
|
}
|
||||||
|
obj["content"] = arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
obj["status"] = statusToString(*status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
if (content.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &c : content) {
|
||||||
|
if (!c.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString roleToString(Role r) noexcept
|
||||||
|
{
|
||||||
|
switch (r) {
|
||||||
|
case Role::User:
|
||||||
|
return "user";
|
||||||
|
case Role::Assistant:
|
||||||
|
return "assistant";
|
||||||
|
case Role::System:
|
||||||
|
return "system";
|
||||||
|
case Role::Developer:
|
||||||
|
return "developer";
|
||||||
|
}
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString statusToString(MessageStatus s) noexcept
|
||||||
|
{
|
||||||
|
switch (s) {
|
||||||
|
case MessageStatus::InProgress:
|
||||||
|
return "in_progress";
|
||||||
|
case MessageStatus::Completed:
|
||||||
|
return "completed";
|
||||||
|
case MessageStatus::Incomplete:
|
||||||
|
return "incomplete";
|
||||||
|
}
|
||||||
|
return "in_progress";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FunctionTool
|
||||||
|
{
|
||||||
|
QString name;
|
||||||
|
QJsonObject parameters;
|
||||||
|
std::optional<QString> description;
|
||||||
|
bool strict = true;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj{{"type", "function"},
|
||||||
|
{"name", name},
|
||||||
|
{"parameters", parameters},
|
||||||
|
{"strict", strict}};
|
||||||
|
if (description)
|
||||||
|
obj["description"] = *description;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return !name.isEmpty() && !parameters.isEmpty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileSearchTool
|
||||||
|
{
|
||||||
|
QStringList vectorStoreIds;
|
||||||
|
std::optional<int> maxNumResults;
|
||||||
|
std::optional<double> scoreThreshold;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj{{"type", "file_search"}};
|
||||||
|
QJsonArray ids;
|
||||||
|
for (const auto &id : vectorStoreIds) {
|
||||||
|
ids.append(id);
|
||||||
|
}
|
||||||
|
obj["vector_store_ids"] = ids;
|
||||||
|
|
||||||
|
if (maxNumResults)
|
||||||
|
obj["max_num_results"] = *maxNumResults;
|
||||||
|
if (scoreThreshold)
|
||||||
|
obj["score_threshold"] = *scoreThreshold;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return !vectorStoreIds.isEmpty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WebSearchTool
|
||||||
|
{
|
||||||
|
QString searchContextSize = "medium";
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
return QJsonObject{{"type", "web_search"}, {"search_context_size", searchContextSize}};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return !searchContextSize.isEmpty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CodeInterpreterTool
|
||||||
|
{
|
||||||
|
QString container;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
return QJsonObject{{"type", "code_interpreter"}, {"container", container}};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return !container.isEmpty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Tool
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Tool(FunctionTool tool) : m_variant(std::move(tool)) {}
|
||||||
|
Tool(FileSearchTool tool) : m_variant(std::move(tool)) {}
|
||||||
|
Tool(WebSearchTool tool) : m_variant(std::move(tool)) {}
|
||||||
|
Tool(CodeInterpreterTool tool) : m_variant(std::move(tool)) {}
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
return std::visit([](const auto &t) { return t.toJson(); }, m_variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return std::visit([](const auto &t) { return t.isValid(); }, m_variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::variant<FunctionTool, FileSearchTool, WebSearchTool, CodeInterpreterTool> m_variant;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TextFormatOptions
|
||||||
|
{
|
||||||
|
TextFormat type = TextFormat::Text;
|
||||||
|
std::optional<QString> name;
|
||||||
|
std::optional<QJsonObject> schema;
|
||||||
|
std::optional<QString> description;
|
||||||
|
std::optional<bool> strict;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case TextFormat::Text:
|
||||||
|
obj["type"] = "text";
|
||||||
|
break;
|
||||||
|
case TextFormat::JsonSchema:
|
||||||
|
obj["type"] = "json_schema";
|
||||||
|
if (name)
|
||||||
|
obj["name"] = *name;
|
||||||
|
if (schema)
|
||||||
|
obj["schema"] = *schema;
|
||||||
|
if (description)
|
||||||
|
obj["description"] = *description;
|
||||||
|
if (strict)
|
||||||
|
obj["strict"] = *strict;
|
||||||
|
break;
|
||||||
|
case TextFormat::JsonObject:
|
||||||
|
obj["type"] = "json_object";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
if (type == TextFormat::JsonSchema) {
|
||||||
|
return name.has_value() && schema.has_value();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
562
providers/OpenAIResponses/ResponseObject.hpp
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <variant>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
enum class ResponseStatus { Completed, Failed, InProgress, Cancelled, Queued, Incomplete };
|
||||||
|
|
||||||
|
enum class ItemStatus { InProgress, Completed, Incomplete };
|
||||||
|
|
||||||
|
struct FileCitation
|
||||||
|
{
|
||||||
|
QString fileId;
|
||||||
|
QString filename;
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
static FileCitation fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
return {obj["file_id"].toString(), obj["filename"].toString(), obj["index"].toInt()};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !fileId.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UrlCitation
|
||||||
|
{
|
||||||
|
QString url;
|
||||||
|
QString title;
|
||||||
|
int startIndex = 0;
|
||||||
|
int endIndex = 0;
|
||||||
|
|
||||||
|
static UrlCitation fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
obj["url"].toString(),
|
||||||
|
obj["title"].toString(),
|
||||||
|
obj["start_index"].toInt(),
|
||||||
|
obj["end_index"].toInt()};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !url.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputText
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
QList<FileCitation> fileCitations;
|
||||||
|
QList<UrlCitation> urlCitations;
|
||||||
|
|
||||||
|
static OutputText fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
OutputText result;
|
||||||
|
result.text = obj["text"].toString();
|
||||||
|
|
||||||
|
if (obj.contains("annotations")) {
|
||||||
|
const QJsonArray annotations = obj["annotations"].toArray();
|
||||||
|
result.fileCitations.reserve(annotations.size());
|
||||||
|
result.urlCitations.reserve(annotations.size());
|
||||||
|
|
||||||
|
for (const auto &annValue : annotations) {
|
||||||
|
const QJsonObject ann = annValue.toObject();
|
||||||
|
const QString type = ann["type"].toString();
|
||||||
|
if (type == "file_citation") {
|
||||||
|
result.fileCitations.append(FileCitation::fromJson(ann));
|
||||||
|
} else if (type == "url_citation") {
|
||||||
|
result.urlCitations.append(UrlCitation::fromJson(ann));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !text.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Refusal
|
||||||
|
{
|
||||||
|
QString refusal;
|
||||||
|
|
||||||
|
static Refusal fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
return {obj["refusal"].toString()};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !refusal.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MessageOutput
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
QString role;
|
||||||
|
ItemStatus status = ItemStatus::InProgress;
|
||||||
|
QList<OutputText> outputTexts;
|
||||||
|
QList<Refusal> refusals;
|
||||||
|
|
||||||
|
static MessageOutput fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
MessageOutput result;
|
||||||
|
result.id = obj["id"].toString();
|
||||||
|
result.role = obj["role"].toString();
|
||||||
|
|
||||||
|
const QString statusStr = obj["status"].toString();
|
||||||
|
if (statusStr == "in_progress")
|
||||||
|
result.status = ItemStatus::InProgress;
|
||||||
|
else if (statusStr == "completed")
|
||||||
|
result.status = ItemStatus::Completed;
|
||||||
|
else
|
||||||
|
result.status = ItemStatus::Incomplete;
|
||||||
|
|
||||||
|
if (obj.contains("content")) {
|
||||||
|
const QJsonArray content = obj["content"].toArray();
|
||||||
|
result.outputTexts.reserve(content.size());
|
||||||
|
result.refusals.reserve(content.size());
|
||||||
|
|
||||||
|
for (const auto &item : content) {
|
||||||
|
const QJsonObject itemObj = item.toObject();
|
||||||
|
const QString type = itemObj["type"].toString();
|
||||||
|
|
||||||
|
if (type == "output_text") {
|
||||||
|
result.outputTexts.append(OutputText::fromJson(itemObj));
|
||||||
|
} else if (type == "refusal") {
|
||||||
|
result.refusals.append(Refusal::fromJson(itemObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||||
|
bool hasContent() const noexcept { return !outputTexts.isEmpty() || !refusals.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FunctionCall
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
QString callId;
|
||||||
|
QString name;
|
||||||
|
QString arguments;
|
||||||
|
ItemStatus status = ItemStatus::InProgress;
|
||||||
|
|
||||||
|
static FunctionCall fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
FunctionCall result;
|
||||||
|
result.id = obj["id"].toString();
|
||||||
|
result.callId = obj["call_id"].toString();
|
||||||
|
result.name = obj["name"].toString();
|
||||||
|
result.arguments = obj["arguments"].toString();
|
||||||
|
|
||||||
|
const QString statusStr = obj["status"].toString();
|
||||||
|
if (statusStr == "in_progress")
|
||||||
|
result.status = ItemStatus::InProgress;
|
||||||
|
else if (statusStr == "completed")
|
||||||
|
result.status = ItemStatus::Completed;
|
||||||
|
else
|
||||||
|
result.status = ItemStatus::Incomplete;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !id.isEmpty() && !callId.isEmpty() && !name.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReasoningOutput
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
ItemStatus status = ItemStatus::InProgress;
|
||||||
|
QString summaryText;
|
||||||
|
QString encryptedContent;
|
||||||
|
QList<QString> contentTexts;
|
||||||
|
|
||||||
|
static ReasoningOutput fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
ReasoningOutput result;
|
||||||
|
result.id = obj["id"].toString();
|
||||||
|
|
||||||
|
const QString statusStr = obj["status"].toString();
|
||||||
|
if (statusStr == "in_progress")
|
||||||
|
result.status = ItemStatus::InProgress;
|
||||||
|
else if (statusStr == "completed")
|
||||||
|
result.status = ItemStatus::Completed;
|
||||||
|
else
|
||||||
|
result.status = ItemStatus::Incomplete;
|
||||||
|
|
||||||
|
if (obj.contains("summary")) {
|
||||||
|
const QJsonArray summary = obj["summary"].toArray();
|
||||||
|
for (const auto &item : summary) {
|
||||||
|
const QJsonObject itemObj = item.toObject();
|
||||||
|
if (itemObj["type"].toString() == "summary_text") {
|
||||||
|
result.summaryText = itemObj["text"].toString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("content")) {
|
||||||
|
const QJsonArray content = obj["content"].toArray();
|
||||||
|
result.contentTexts.reserve(content.size());
|
||||||
|
|
||||||
|
for (const auto &item : content) {
|
||||||
|
const QJsonObject itemObj = item.toObject();
|
||||||
|
if (itemObj["type"].toString() == "reasoning_text") {
|
||||||
|
result.contentTexts.append(itemObj["text"].toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("encrypted_content")) {
|
||||||
|
result.encryptedContent = obj["encrypted_content"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||||
|
bool hasContent() const noexcept
|
||||||
|
{
|
||||||
|
return !summaryText.isEmpty() || !contentTexts.isEmpty() || !encryptedContent.isEmpty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileSearchResult
|
||||||
|
{
|
||||||
|
QString fileId;
|
||||||
|
QString filename;
|
||||||
|
QString text;
|
||||||
|
double score = 0.0;
|
||||||
|
|
||||||
|
static FileSearchResult fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
obj["file_id"].toString(),
|
||||||
|
obj["filename"].toString(),
|
||||||
|
obj["text"].toString(),
|
||||||
|
obj["score"].toDouble()};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !fileId.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileSearchCall
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
QString status;
|
||||||
|
QStringList queries;
|
||||||
|
QList<FileSearchResult> results;
|
||||||
|
|
||||||
|
static FileSearchCall fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
FileSearchCall result;
|
||||||
|
result.id = obj["id"].toString();
|
||||||
|
result.status = obj["status"].toString();
|
||||||
|
|
||||||
|
if (obj.contains("queries")) {
|
||||||
|
const QJsonArray queries = obj["queries"].toArray();
|
||||||
|
result.queries.reserve(queries.size());
|
||||||
|
|
||||||
|
for (const auto &q : queries) {
|
||||||
|
result.queries.append(q.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("results")) {
|
||||||
|
const QJsonArray results = obj["results"].toArray();
|
||||||
|
result.results.reserve(results.size());
|
||||||
|
|
||||||
|
for (const auto &r : results) {
|
||||||
|
result.results.append(FileSearchResult::fromJson(r.toObject()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CodeInterpreterOutput
|
||||||
|
{
|
||||||
|
QString type;
|
||||||
|
QString logs;
|
||||||
|
QString imageUrl;
|
||||||
|
|
||||||
|
static CodeInterpreterOutput fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
CodeInterpreterOutput result;
|
||||||
|
result.type = obj["type"].toString();
|
||||||
|
if (result.type == "logs") {
|
||||||
|
result.logs = obj["logs"].toString();
|
||||||
|
} else if (result.type == "image") {
|
||||||
|
result.imageUrl = obj["url"].toString();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept
|
||||||
|
{
|
||||||
|
return !type.isEmpty() && (!logs.isEmpty() || !imageUrl.isEmpty());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CodeInterpreterCall
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
QString containerId;
|
||||||
|
std::optional<QString> code;
|
||||||
|
QString status;
|
||||||
|
QList<CodeInterpreterOutput> outputs;
|
||||||
|
|
||||||
|
static CodeInterpreterCall fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
CodeInterpreterCall result;
|
||||||
|
result.id = obj["id"].toString();
|
||||||
|
result.containerId = obj["container_id"].toString();
|
||||||
|
result.status = obj["status"].toString();
|
||||||
|
|
||||||
|
if (obj.contains("code") && !obj["code"].isNull()) {
|
||||||
|
result.code = obj["code"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("outputs")) {
|
||||||
|
const QJsonArray outputs = obj["outputs"].toArray();
|
||||||
|
result.outputs.reserve(outputs.size());
|
||||||
|
|
||||||
|
for (const auto &o : outputs) {
|
||||||
|
result.outputs.append(CodeInterpreterOutput::fromJson(o.toObject()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !id.isEmpty() && !containerId.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class OutputItem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum class Type { Message, FunctionCall, Reasoning, FileSearch, CodeInterpreter, Unknown };
|
||||||
|
|
||||||
|
explicit OutputItem(const MessageOutput &msg)
|
||||||
|
: m_type(Type::Message)
|
||||||
|
, m_data(msg)
|
||||||
|
{}
|
||||||
|
explicit OutputItem(const FunctionCall &call)
|
||||||
|
: m_type(Type::FunctionCall)
|
||||||
|
, m_data(call)
|
||||||
|
{}
|
||||||
|
explicit OutputItem(const ReasoningOutput &reasoning)
|
||||||
|
: m_type(Type::Reasoning)
|
||||||
|
, m_data(reasoning)
|
||||||
|
{}
|
||||||
|
explicit OutputItem(const FileSearchCall &search)
|
||||||
|
: m_type(Type::FileSearch)
|
||||||
|
, m_data(search)
|
||||||
|
{}
|
||||||
|
explicit OutputItem(const CodeInterpreterCall &interpreter)
|
||||||
|
: m_type(Type::CodeInterpreter)
|
||||||
|
, m_data(interpreter)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Type type() const { return m_type; }
|
||||||
|
|
||||||
|
const MessageOutput *asMessage() const
|
||||||
|
{
|
||||||
|
return std::holds_alternative<MessageOutput>(m_data) ? &std::get<MessageOutput>(m_data)
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FunctionCall *asFunctionCall() const
|
||||||
|
{
|
||||||
|
return std::holds_alternative<FunctionCall>(m_data) ? &std::get<FunctionCall>(m_data)
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReasoningOutput *asReasoning() const
|
||||||
|
{
|
||||||
|
return std::holds_alternative<ReasoningOutput>(m_data) ? &std::get<ReasoningOutput>(m_data)
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileSearchCall *asFileSearch() const
|
||||||
|
{
|
||||||
|
return std::holds_alternative<FileSearchCall>(m_data) ? &std::get<FileSearchCall>(m_data)
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CodeInterpreterCall *asCodeInterpreter() const
|
||||||
|
{
|
||||||
|
return std::holds_alternative<CodeInterpreterCall>(m_data)
|
||||||
|
? &std::get<CodeInterpreterCall>(m_data)
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static OutputItem fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
const QString type = obj["type"].toString();
|
||||||
|
|
||||||
|
if (type == "message") {
|
||||||
|
return OutputItem(MessageOutput::fromJson(obj));
|
||||||
|
} else if (type == "function_call") {
|
||||||
|
return OutputItem(FunctionCall::fromJson(obj));
|
||||||
|
} else if (type == "reasoning") {
|
||||||
|
return OutputItem(ReasoningOutput::fromJson(obj));
|
||||||
|
} else if (type == "file_search_call") {
|
||||||
|
return OutputItem(FileSearchCall::fromJson(obj));
|
||||||
|
} else if (type == "code_interpreter_call") {
|
||||||
|
return OutputItem(CodeInterpreterCall::fromJson(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
return OutputItem(MessageOutput{});
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Type m_type;
|
||||||
|
std::variant<MessageOutput, FunctionCall, ReasoningOutput, FileSearchCall, CodeInterpreterCall>
|
||||||
|
m_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Usage
|
||||||
|
{
|
||||||
|
int inputTokens = 0;
|
||||||
|
int outputTokens = 0;
|
||||||
|
int totalTokens = 0;
|
||||||
|
|
||||||
|
static Usage fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
obj["input_tokens"].toInt(),
|
||||||
|
obj["output_tokens"].toInt(),
|
||||||
|
obj["total_tokens"].toInt()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return totalTokens > 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResponseError
|
||||||
|
{
|
||||||
|
QString code;
|
||||||
|
QString message;
|
||||||
|
|
||||||
|
static ResponseError fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
return {obj["code"].toString(), obj["message"].toString()};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !code.isEmpty() && !message.isEmpty(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Response
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
qint64 createdAt = 0;
|
||||||
|
QString model;
|
||||||
|
ResponseStatus status = ResponseStatus::InProgress;
|
||||||
|
QList<OutputItem> output;
|
||||||
|
QString outputText;
|
||||||
|
std::optional<Usage> usage;
|
||||||
|
std::optional<ResponseError> error;
|
||||||
|
std::optional<QString> conversationId;
|
||||||
|
|
||||||
|
static Response fromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
Response result;
|
||||||
|
result.id = obj["id"].toString();
|
||||||
|
result.createdAt = obj["created_at"].toInteger();
|
||||||
|
result.model = obj["model"].toString();
|
||||||
|
|
||||||
|
const QString statusStr = obj["status"].toString();
|
||||||
|
if (statusStr == "completed")
|
||||||
|
result.status = ResponseStatus::Completed;
|
||||||
|
else if (statusStr == "failed")
|
||||||
|
result.status = ResponseStatus::Failed;
|
||||||
|
else if (statusStr == "in_progress")
|
||||||
|
result.status = ResponseStatus::InProgress;
|
||||||
|
else if (statusStr == "cancelled")
|
||||||
|
result.status = ResponseStatus::Cancelled;
|
||||||
|
else if (statusStr == "queued")
|
||||||
|
result.status = ResponseStatus::Queued;
|
||||||
|
else
|
||||||
|
result.status = ResponseStatus::Incomplete;
|
||||||
|
|
||||||
|
if (obj.contains("output")) {
|
||||||
|
const QJsonArray output = obj["output"].toArray();
|
||||||
|
result.output.reserve(output.size());
|
||||||
|
|
||||||
|
for (const auto &item : output) {
|
||||||
|
result.output.append(OutputItem::fromJson(item.toObject()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("output_text")) {
|
||||||
|
result.outputText = obj["output_text"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("usage")) {
|
||||||
|
result.usage = Usage::fromJson(obj["usage"].toObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("error")) {
|
||||||
|
result.error = ResponseError::fromJson(obj["error"].toObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.contains("conversation")) {
|
||||||
|
const QJsonObject conv = obj["conversation"].toObject();
|
||||||
|
result.conversationId = conv["id"].toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getAggregatedText() const
|
||||||
|
{
|
||||||
|
if (!outputText.isEmpty()) {
|
||||||
|
return outputText;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString aggregated;
|
||||||
|
for (const auto &item : output) {
|
||||||
|
if (const auto *msg = item.asMessage()) {
|
||||||
|
for (const auto &text : msg->outputTexts) {
|
||||||
|
aggregated += text.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aggregated;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid() const noexcept { return !id.isEmpty(); }
|
||||||
|
bool hasError() const noexcept { return error.has_value(); }
|
||||||
|
bool isCompleted() const noexcept { return status == ResponseStatus::Completed; }
|
||||||
|
bool isFailed() const noexcept { return status == ResponseStatus::Failed; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
246
providers/OpenAIResponsesMessage.cpp
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "OpenAIResponsesMessage.hpp"
|
||||||
|
#include "OpenAIResponses/ResponseObject.hpp"
|
||||||
|
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
OpenAIResponsesMessage::OpenAIResponsesMessage(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleItemDelta(const QJsonObject &item)
|
||||||
|
{
|
||||||
|
using namespace QodeAssist::OpenAIResponses;
|
||||||
|
|
||||||
|
const QString itemType = item["type"].toString();
|
||||||
|
|
||||||
|
if (itemType == "message" || (itemType.isEmpty() && item.contains("content"))) {
|
||||||
|
OutputItem outputItem = OutputItem::fromJson(item);
|
||||||
|
|
||||||
|
if (const auto *msg = outputItem.asMessage()) {
|
||||||
|
for (const auto &outputText : msg->outputTexts) {
|
||||||
|
if (!outputText.text.isEmpty()) {
|
||||||
|
auto textItem = getOrCreateTextItem();
|
||||||
|
textItem->appendText(outputText.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleToolCallStart(const QString &callId, const QString &name)
|
||||||
|
{
|
||||||
|
auto toolContent = new LLMCore::ToolUseContent(callId, name);
|
||||||
|
toolContent->setParent(this);
|
||||||
|
m_items.append(toolContent);
|
||||||
|
m_toolCalls[callId] = toolContent;
|
||||||
|
m_pendingToolArguments[callId] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleToolCallDelta(const QString &callId, const QString &argumentsDelta)
|
||||||
|
{
|
||||||
|
if (m_pendingToolArguments.contains(callId)) {
|
||||||
|
m_pendingToolArguments[callId] += argumentsDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleToolCallComplete(const QString &callId)
|
||||||
|
{
|
||||||
|
if (m_pendingToolArguments.contains(callId) && m_toolCalls.contains(callId)) {
|
||||||
|
QString jsonArgs = m_pendingToolArguments[callId];
|
||||||
|
QJsonObject argsObject;
|
||||||
|
|
||||||
|
if (!jsonArgs.isEmpty()) {
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonArgs.toUtf8());
|
||||||
|
if (doc.isObject()) {
|
||||||
|
argsObject = doc.object();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_toolCalls[callId]->setInput(argsObject);
|
||||||
|
m_pendingToolArguments.remove(callId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleReasoningStart(const QString &itemId)
|
||||||
|
{
|
||||||
|
auto thinkingContent = new LLMCore::ThinkingContent();
|
||||||
|
thinkingContent->setParent(this);
|
||||||
|
m_items.append(thinkingContent);
|
||||||
|
m_thinkingBlocks[itemId] = thinkingContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleReasoningDelta(const QString &itemId, const QString &text)
|
||||||
|
{
|
||||||
|
if (m_thinkingBlocks.contains(itemId)) {
|
||||||
|
m_thinkingBlocks[itemId]->appendThinking(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleReasoningComplete(const QString &itemId)
|
||||||
|
{
|
||||||
|
Q_UNUSED(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::handleStatus(const QString &status)
|
||||||
|
{
|
||||||
|
m_status = status;
|
||||||
|
updateStateFromStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QJsonObject> OpenAIResponsesMessage::toItemsFormat() const
|
||||||
|
{
|
||||||
|
QList<QJsonObject> items;
|
||||||
|
|
||||||
|
QString textContent;
|
||||||
|
QList<LLMCore::ToolUseContent *> toolCalls;
|
||||||
|
|
||||||
|
for (const auto *block : m_items) {
|
||||||
|
if (const auto *text = qobject_cast<const LLMCore::TextContent *>(block)) {
|
||||||
|
textContent += text->text();
|
||||||
|
} else if (auto *tool = qobject_cast<LLMCore::ToolUseContent *>(
|
||||||
|
const_cast<LLMCore::ContentBlock *>(block))) {
|
||||||
|
toolCalls.append(tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!textContent.isEmpty()) {
|
||||||
|
QJsonObject message;
|
||||||
|
message["role"] = "assistant";
|
||||||
|
message["content"] = textContent;
|
||||||
|
items.append(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto *tool : toolCalls) {
|
||||||
|
QJsonObject functionCallItem;
|
||||||
|
functionCallItem["type"] = "function_call";
|
||||||
|
functionCallItem["call_id"] = tool->id();
|
||||||
|
functionCallItem["name"] = tool->name();
|
||||||
|
functionCallItem["arguments"] = QString::fromUtf8(
|
||||||
|
QJsonDocument(tool->input()).toJson(QJsonDocument::Compact));
|
||||||
|
items.append(functionCallItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<LLMCore::ToolUseContent *> OpenAIResponsesMessage::getCurrentToolUseContent() const
|
||||||
|
{
|
||||||
|
QList<LLMCore::ToolUseContent *> toolBlocks;
|
||||||
|
for (auto *block : m_items) {
|
||||||
|
if (auto *toolContent = qobject_cast<LLMCore::ToolUseContent *>(block)) {
|
||||||
|
toolBlocks.append(toolContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toolBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<LLMCore::ThinkingContent *> OpenAIResponsesMessage::getCurrentThinkingContent() const
|
||||||
|
{
|
||||||
|
QList<LLMCore::ThinkingContent *> thinkingBlocks;
|
||||||
|
for (auto *block : m_items) {
|
||||||
|
if (auto *thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(block)) {
|
||||||
|
thinkingBlocks.append(thinkingContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return thinkingBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonArray OpenAIResponsesMessage::createToolResultItems(const QHash<QString, QString> &toolResults) const
|
||||||
|
{
|
||||||
|
QJsonArray items;
|
||||||
|
|
||||||
|
for (const auto *toolContent : getCurrentToolUseContent()) {
|
||||||
|
if (toolResults.contains(toolContent->id())) {
|
||||||
|
QJsonObject toolResultItem;
|
||||||
|
toolResultItem["type"] = "function_call_output";
|
||||||
|
toolResultItem["call_id"] = toolContent->id();
|
||||||
|
toolResultItem["output"] = toolResults[toolContent->id()];
|
||||||
|
items.append(toolResultItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIResponsesMessage::accumulatedText() const
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
for (const auto *block : m_items) {
|
||||||
|
if (const auto *textContent = qobject_cast<const LLMCore::TextContent *>(block)) {
|
||||||
|
text += textContent->text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::updateStateFromStatus()
|
||||||
|
{
|
||||||
|
using namespace QodeAssist::OpenAIResponses;
|
||||||
|
|
||||||
|
if (m_status == "completed") {
|
||||||
|
if (!getCurrentToolUseContent().isEmpty()) {
|
||||||
|
m_state = LLMCore::MessageState::RequiresToolExecution;
|
||||||
|
} else {
|
||||||
|
m_state = LLMCore::MessageState::Complete;
|
||||||
|
}
|
||||||
|
} else if (m_status == "in_progress") {
|
||||||
|
m_state = LLMCore::MessageState::Building;
|
||||||
|
} else if (m_status == "failed" || m_status == "cancelled" || m_status == "incomplete") {
|
||||||
|
m_state = LLMCore::MessageState::Final;
|
||||||
|
} else {
|
||||||
|
m_state = LLMCore::MessageState::Building;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LLMCore::TextContent *OpenAIResponsesMessage::getOrCreateTextItem()
|
||||||
|
{
|
||||||
|
for (auto *block : m_items) {
|
||||||
|
if (auto *textContent = qobject_cast<LLMCore::TextContent *>(block)) {
|
||||||
|
return textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *textContent = new LLMCore::TextContent();
|
||||||
|
textContent->setParent(this);
|
||||||
|
m_items.append(textContent);
|
||||||
|
return textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesMessage::startNewContinuation()
|
||||||
|
{
|
||||||
|
m_toolCalls.clear();
|
||||||
|
m_thinkingBlocks.clear();
|
||||||
|
|
||||||
|
qDeleteAll(m_items);
|
||||||
|
m_items.clear();
|
||||||
|
|
||||||
|
m_pendingToolArguments.clear();
|
||||||
|
m_status.clear();
|
||||||
|
m_state = LLMCore::MessageState::Building;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
|
|
||||||
67
providers/OpenAIResponsesMessage.hpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <llmcore/ContentBlocks.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
class OpenAIResponsesMessage : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit OpenAIResponsesMessage(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void handleItemDelta(const QJsonObject &item);
|
||||||
|
void handleToolCallStart(const QString &callId, const QString &name);
|
||||||
|
void handleToolCallDelta(const QString &callId, const QString &argumentsDelta);
|
||||||
|
void handleToolCallComplete(const QString &callId);
|
||||||
|
void handleReasoningStart(const QString &itemId);
|
||||||
|
void handleReasoningDelta(const QString &itemId, const QString &text);
|
||||||
|
void handleReasoningComplete(const QString &itemId);
|
||||||
|
void handleStatus(const QString &status);
|
||||||
|
|
||||||
|
QList<QJsonObject> toItemsFormat() const;
|
||||||
|
QJsonArray createToolResultItems(const QHash<QString, QString> &toolResults) const;
|
||||||
|
|
||||||
|
LLMCore::MessageState state() const noexcept { return m_state; }
|
||||||
|
QString accumulatedText() const;
|
||||||
|
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
|
||||||
|
QList<LLMCore::ThinkingContent *> getCurrentThinkingContent() const;
|
||||||
|
|
||||||
|
bool hasToolCalls() const noexcept { return !m_toolCalls.isEmpty(); }
|
||||||
|
bool hasThinkingContent() const noexcept { return !m_thinkingBlocks.isEmpty(); }
|
||||||
|
|
||||||
|
void startNewContinuation();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_status;
|
||||||
|
LLMCore::MessageState m_state = LLMCore::MessageState::Building;
|
||||||
|
QList<LLMCore::ContentBlock *> m_items;
|
||||||
|
QHash<QString, QString> m_pendingToolArguments;
|
||||||
|
QHash<QString, LLMCore::ToolUseContent *> m_toolCalls;
|
||||||
|
QHash<QString, LLMCore::ThinkingContent *> m_thinkingBlocks;
|
||||||
|
|
||||||
|
void updateStateFromStatus();
|
||||||
|
LLMCore::TextContent *getOrCreateTextItem();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
|
|
||||||
651
providers/OpenAIResponsesProvider.cpp
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "OpenAIResponsesProvider.hpp"
|
||||||
|
#include "OpenAIResponses/ResponseObject.hpp"
|
||||||
|
|
||||||
|
#include "llmcore/ValidationUtils.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
|
#include "settings/CodeCompletionSettings.hpp"
|
||||||
|
#include "settings/GeneralSettings.hpp"
|
||||||
|
#include "settings/ProviderSettings.hpp"
|
||||||
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
OpenAIResponsesProvider::OpenAIResponsesProvider(QObject *parent)
|
||||||
|
: LLMCore::Provider(parent)
|
||||||
|
, m_toolsManager(new Tools::ToolsManager(this))
|
||||||
|
{
|
||||||
|
connect(
|
||||||
|
m_toolsManager,
|
||||||
|
&Tools::ToolsManager::toolExecutionComplete,
|
||||||
|
this,
|
||||||
|
&OpenAIResponsesProvider::onToolExecutionComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIResponsesProvider::name() const
|
||||||
|
{
|
||||||
|
return "OpenAI Responses";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIResponsesProvider::url() const
|
||||||
|
{
|
||||||
|
return "https://api.openai.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIResponsesProvider::completionEndpoint() const
|
||||||
|
{
|
||||||
|
return "/v1/responses";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIResponsesProvider::chatEndpoint() const
|
||||||
|
{
|
||||||
|
return "/v1/responses";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenAIResponsesProvider::supportsModelListing() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::prepareRequest(
|
||||||
|
QJsonObject &request,
|
||||||
|
LLMCore::PromptTemplate *prompt,
|
||||||
|
LLMCore::ContextData context,
|
||||||
|
LLMCore::RequestType type,
|
||||||
|
bool isToolsEnabled,
|
||||||
|
bool isThinkingEnabled)
|
||||||
|
{
|
||||||
|
if (!prompt->isSupportProvider(providerID())) {
|
||||||
|
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt->prepareRequest(request, context);
|
||||||
|
|
||||||
|
auto applyModelParams = [&request](const auto &settings) {
|
||||||
|
request["max_output_tokens"] = settings.maxTokens();
|
||||||
|
|
||||||
|
if (settings.useTopP()) {
|
||||||
|
request["top_p"] = settings.topP();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto applyThinkingMode = [&request](const auto &settings) {
|
||||||
|
QString effortStr = settings.openAIResponsesReasoningEffort.stringValue().toLower();
|
||||||
|
if (effortStr.isEmpty()) {
|
||||||
|
effortStr = "medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject reasoning;
|
||||||
|
reasoning["effort"] = effortStr;
|
||||||
|
request["reasoning"] = reasoning;
|
||||||
|
request["max_output_tokens"] = settings.thinkingMaxTokens();
|
||||||
|
request["store"] = true;
|
||||||
|
|
||||||
|
QJsonArray include;
|
||||||
|
include.append("reasoning.encrypted_content");
|
||||||
|
request["include"] = include;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (type == LLMCore::RequestType::CodeCompletion) {
|
||||||
|
applyModelParams(Settings::codeCompletionSettings());
|
||||||
|
} else if (type == LLMCore::RequestType::QuickRefactoring) {
|
||||||
|
const auto &qrSettings = Settings::quickRefactorSettings();
|
||||||
|
applyModelParams(qrSettings);
|
||||||
|
|
||||||
|
if (isThinkingEnabled) {
|
||||||
|
applyThinkingMode(qrSettings);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const auto &chatSettings = Settings::chatAssistantSettings();
|
||||||
|
applyModelParams(chatSettings);
|
||||||
|
|
||||||
|
if (isThinkingEnabled) {
|
||||||
|
applyThinkingMode(chatSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isToolsEnabled) {
|
||||||
|
const LLMCore::RunToolsFilter filter = (type == LLMCore::RequestType::QuickRefactoring)
|
||||||
|
? LLMCore::RunToolsFilter::OnlyRead
|
||||||
|
: LLMCore::RunToolsFilter::ALL;
|
||||||
|
|
||||||
|
const auto toolsDefinitions
|
||||||
|
= m_toolsManager->getToolsDefinitions(LLMCore::ToolSchemaFormat::OpenAI, filter);
|
||||||
|
if (!toolsDefinitions.isEmpty()) {
|
||||||
|
QJsonArray responsesTools;
|
||||||
|
|
||||||
|
for (const QJsonValue &toolValue : toolsDefinitions) {
|
||||||
|
const QJsonObject tool = toolValue.toObject();
|
||||||
|
if (tool.contains("function")) {
|
||||||
|
const QJsonObject functionObj = tool["function"].toObject();
|
||||||
|
QJsonObject responsesTool;
|
||||||
|
responsesTool["type"] = "function";
|
||||||
|
responsesTool["name"] = functionObj["name"];
|
||||||
|
responsesTool["description"] = functionObj["description"];
|
||||||
|
responsesTool["parameters"] = functionObj["parameters"];
|
||||||
|
responsesTools.append(responsesTool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request["tools"] = responsesTools;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request["stream"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<QList<QString>> OpenAIResponsesProvider::getInstalledModels(const QString &url)
|
||||||
|
{
|
||||||
|
QNetworkRequest request(QString("%1/v1/models").arg(url));
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
if (!apiKey().isEmpty()) {
|
||||||
|
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||||
|
QList<QString> models;
|
||||||
|
const QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||||
|
|
||||||
|
if (jsonObject.contains("data")) {
|
||||||
|
const QJsonArray modelArray = jsonObject["data"].toArray();
|
||||||
|
models.reserve(modelArray.size());
|
||||||
|
|
||||||
|
static const QStringList modelPrefixes = {"gpt-5", "o1", "o2", "o3", "o4"};
|
||||||
|
|
||||||
|
for (const QJsonValue &value : modelArray) {
|
||||||
|
const QJsonObject modelObject = value.toObject();
|
||||||
|
if (!modelObject.contains("id")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString modelId = modelObject["id"].toString();
|
||||||
|
for (const QString &prefix : modelPrefixes) {
|
||||||
|
if (modelId.contains(prefix)) {
|
||||||
|
models.append(modelId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
}).onFailed([](const std::exception &e) {
|
||||||
|
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what()));
|
||||||
|
return QList<QString>{};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QString> OpenAIResponsesProvider::validateRequest(
|
||||||
|
const QJsonObject &request, LLMCore::TemplateType type)
|
||||||
|
{
|
||||||
|
Q_UNUSED(type);
|
||||||
|
|
||||||
|
QList<QString> errors;
|
||||||
|
|
||||||
|
if (!request.contains("input")) {
|
||||||
|
errors.append("Missing required field: input");
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonValue inputValue = request["input"];
|
||||||
|
if (!inputValue.isString() && !inputValue.isArray()) {
|
||||||
|
errors.append("Field 'input' must be either a string or an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.contains("max_output_tokens") && !request["max_output_tokens"].isDouble()) {
|
||||||
|
errors.append("Field 'max_output_tokens' must be a number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.contains("top_p") && !request["top_p"].isDouble()) {
|
||||||
|
errors.append("Field 'top_p' must be a number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.contains("reasoning") && !request["reasoning"].isObject()) {
|
||||||
|
errors.append("Field 'reasoning' must be an object");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.contains("stream") && !request["stream"].isBool()) {
|
||||||
|
errors.append("Field 'stream' must be a boolean");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.contains("tools") && !request["tools"].isArray()) {
|
||||||
|
errors.append("Field 'tools' must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString OpenAIResponsesProvider::apiKey() const
|
||||||
|
{
|
||||||
|
return Settings::providerSettings().openAiApiKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
|
||||||
|
{
|
||||||
|
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
|
|
||||||
|
if (!apiKey().isEmpty()) {
|
||||||
|
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LLMCore::ProviderID OpenAIResponsesProvider::providerID() const
|
||||||
|
{
|
||||||
|
return LLMCore::ProviderID::OpenAIResponses;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::sendRequest(
|
||||||
|
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
|
||||||
|
{
|
||||||
|
if (!m_messages.contains(requestId)) {
|
||||||
|
m_dataBuffers[requestId].clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_requestUrls[requestId] = url;
|
||||||
|
m_originalRequests[requestId] = payload;
|
||||||
|
|
||||||
|
QNetworkRequest networkRequest(url);
|
||||||
|
prepareNetworkRequest(networkRequest);
|
||||||
|
|
||||||
|
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenAIResponsesProvider::supportsTools() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenAIResponsesProvider::supportImage() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenAIResponsesProvider::supportThinking() const
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::cancelRequest(const LLMCore::RequestID &requestId)
|
||||||
|
{
|
||||||
|
LLMCore::Provider::cancelRequest(requestId);
|
||||||
|
cleanupRequest(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::onDataReceived(
|
||||||
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||||
|
{
|
||||||
|
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||||
|
const QStringList lines = buffers.rawStreamBuffer.processData(data);
|
||||||
|
|
||||||
|
QString currentEventType;
|
||||||
|
|
||||||
|
for (const QString &line : lines) {
|
||||||
|
const QString trimmedLine = line.trimmed();
|
||||||
|
if (trimmedLine.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line == "data: [DONE]") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("event: ")) {
|
||||||
|
currentEventType = line.mid(7).trimmed();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString dataLine = line;
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
dataLine = line.mid(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(dataLine.toUtf8());
|
||||||
|
if (doc.isObject()) {
|
||||||
|
const QJsonObject obj = doc.object();
|
||||||
|
processStreamEvent(requestId, currentEventType, obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::onRequestFinished(
|
||||||
|
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||||
|
{
|
||||||
|
if (error) {
|
||||||
|
LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, *error));
|
||||||
|
emit requestFailed(requestId, *error);
|
||||||
|
cleanupRequest(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_messages.contains(requestId)) {
|
||||||
|
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||||
|
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_dataBuffers.contains(requestId)) {
|
||||||
|
const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
|
||||||
|
if (!buffers.responseContent.isEmpty()) {
|
||||||
|
emit fullResponseReceived(requestId, buffers.responseContent);
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("WARNING: OpenAIResponses - Response content is empty for %1, "
|
||||||
|
"emitting empty response")
|
||||||
|
.arg(requestId));
|
||||||
|
emit fullResponseReceived(requestId, "");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("WARNING: OpenAIResponses - No data buffer found for %1").arg(requestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupRequest(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::processStreamEvent(
|
||||||
|
const QString &requestId, const QString &eventType, const QJsonObject &data)
|
||||||
|
{
|
||||||
|
OpenAIResponsesMessage *message = m_messages.value(requestId);
|
||||||
|
if (!message) {
|
||||||
|
message = new OpenAIResponsesMessage(this);
|
||||||
|
m_messages[requestId] = message;
|
||||||
|
|
||||||
|
if (m_dataBuffers.contains(requestId)) {
|
||||||
|
emit continuationStarted(requestId);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
m_dataBuffers.contains(requestId)
|
||||||
|
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||||
|
message->startNewContinuation();
|
||||||
|
emit continuationStarted(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType == "response.content_part.added") {
|
||||||
|
} else if (eventType == "response.output_text.delta") {
|
||||||
|
const QString delta = data["delta"].toString();
|
||||||
|
if (!delta.isEmpty()) {
|
||||||
|
m_dataBuffers[requestId].responseContent += delta;
|
||||||
|
emit partialResponseReceived(requestId, delta);
|
||||||
|
}
|
||||||
|
} else if (eventType == "response.output_text.done") {
|
||||||
|
const QString fullText = data["text"].toString();
|
||||||
|
if (!fullText.isEmpty()) {
|
||||||
|
m_dataBuffers[requestId].responseContent = fullText;
|
||||||
|
}
|
||||||
|
} else if (eventType == "response.content_part.done") {
|
||||||
|
} else if (eventType == "response.output_item.added") {
|
||||||
|
using namespace QodeAssist::OpenAIResponses;
|
||||||
|
const QJsonObject item = data["item"].toObject();
|
||||||
|
OutputItem outputItem = OutputItem::fromJson(item);
|
||||||
|
|
||||||
|
if (const auto *functionCall = outputItem.asFunctionCall()) {
|
||||||
|
if (!functionCall->callId.isEmpty() && !functionCall->name.isEmpty()) {
|
||||||
|
if (!m_itemIdToCallId.contains(requestId)) {
|
||||||
|
m_itemIdToCallId[requestId] = QHash<QString, QString>();
|
||||||
|
}
|
||||||
|
m_itemIdToCallId[requestId][functionCall->id] = functionCall->callId;
|
||||||
|
message->handleToolCallStart(functionCall->callId, functionCall->name);
|
||||||
|
}
|
||||||
|
} else if (const auto *reasoning = outputItem.asReasoning()) {
|
||||||
|
if (!reasoning->id.isEmpty()) {
|
||||||
|
message->handleReasoningStart(reasoning->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (eventType == "response.reasoning_content.delta") {
|
||||||
|
const QString itemId = data["item_id"].toString();
|
||||||
|
const QString delta = data["delta"].toString();
|
||||||
|
if (!itemId.isEmpty() && !delta.isEmpty()) {
|
||||||
|
message->handleReasoningDelta(itemId, delta);
|
||||||
|
}
|
||||||
|
} else if (eventType == "response.reasoning_content.done") {
|
||||||
|
const QString itemId = data["item_id"].toString();
|
||||||
|
if (!itemId.isEmpty()) {
|
||||||
|
message->handleReasoningComplete(itemId);
|
||||||
|
emitPendingThinkingBlocks(requestId);
|
||||||
|
}
|
||||||
|
} else if (eventType == "response.function_call_arguments.delta") {
|
||||||
|
const QString itemId = data["item_id"].toString();
|
||||||
|
const QString delta = data["delta"].toString();
|
||||||
|
if (!itemId.isEmpty() && !delta.isEmpty()) {
|
||||||
|
const QString callId = m_itemIdToCallId.value(requestId).value(itemId);
|
||||||
|
if (!callId.isEmpty()) {
|
||||||
|
message->handleToolCallDelta(callId, delta);
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("ERROR: No call_id mapping found for item_id: %1").arg(itemId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
eventType == "response.function_call_arguments.done"
|
||||||
|
|| eventType == "response.output_item.done") {
|
||||||
|
const QString itemId = data["item_id"].toString();
|
||||||
|
const QJsonObject item = data["item"].toObject();
|
||||||
|
|
||||||
|
if (!item.isEmpty() && item["type"].toString() == "reasoning") {
|
||||||
|
using namespace QodeAssist::OpenAIResponses;
|
||||||
|
|
||||||
|
const QString finalItemId = itemId.isEmpty() ? item["id"].toString() : itemId;
|
||||||
|
|
||||||
|
ReasoningOutput reasoningOutput = ReasoningOutput::fromJson(item);
|
||||||
|
QString reasoningText;
|
||||||
|
|
||||||
|
if (!reasoningOutput.summaryText.isEmpty()) {
|
||||||
|
reasoningText = reasoningOutput.summaryText;
|
||||||
|
} else if (!reasoningOutput.contentTexts.isEmpty()) {
|
||||||
|
reasoningText = reasoningOutput.contentTexts.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reasoningText.isEmpty()) {
|
||||||
|
reasoningText = QString(
|
||||||
|
"[Reasoning process completed, but detailed thinking is not available in "
|
||||||
|
"streaming mode. The model has processed your request with extended reasoning.]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!finalItemId.isEmpty()) {
|
||||||
|
message->handleReasoningDelta(finalItemId, reasoningText);
|
||||||
|
message->handleReasoningComplete(finalItemId);
|
||||||
|
emitPendingThinkingBlocks(requestId);
|
||||||
|
}
|
||||||
|
} else if (item.isEmpty() && !itemId.isEmpty()) {
|
||||||
|
const QString callId = m_itemIdToCallId.value(requestId).value(itemId);
|
||||||
|
if (!callId.isEmpty()) {
|
||||||
|
message->handleToolCallComplete(callId);
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("ERROR: OpenAIResponses - No call_id mapping found for item_id: %1")
|
||||||
|
.arg(itemId));
|
||||||
|
}
|
||||||
|
} else if (!item.isEmpty() && item["type"].toString() == "function_call") {
|
||||||
|
const QString callId = item["call_id"].toString();
|
||||||
|
if (!callId.isEmpty()) {
|
||||||
|
message->handleToolCallComplete(callId);
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("ERROR: OpenAIResponses - Function call done but call_id is empty"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (eventType == "response.created") {
|
||||||
|
} else if (eventType == "response.in_progress") {
|
||||||
|
} else if (eventType == "response.completed") {
|
||||||
|
using namespace QodeAssist::OpenAIResponses;
|
||||||
|
const QJsonObject responseObj = data["response"].toObject();
|
||||||
|
Response response = Response::fromJson(responseObj);
|
||||||
|
|
||||||
|
const QString statusStr = responseObj["status"].toString();
|
||||||
|
|
||||||
|
if (m_dataBuffers[requestId].responseContent.isEmpty()) {
|
||||||
|
const QString aggregatedText = response.getAggregatedText();
|
||||||
|
if (!aggregatedText.isEmpty()) {
|
||||||
|
m_dataBuffers[requestId].responseContent = aggregatedText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message->handleStatus(statusStr);
|
||||||
|
handleMessageComplete(requestId);
|
||||||
|
} else if (eventType == "response.incomplete") {
|
||||||
|
using namespace QodeAssist::OpenAIResponses;
|
||||||
|
const QJsonObject responseObj = data["response"].toObject();
|
||||||
|
|
||||||
|
if (!responseObj.isEmpty()) {
|
||||||
|
Response response = Response::fromJson(responseObj);
|
||||||
|
const QString statusStr = responseObj["status"].toString();
|
||||||
|
|
||||||
|
if (m_dataBuffers[requestId].responseContent.isEmpty()) {
|
||||||
|
const QString aggregatedText = response.getAggregatedText();
|
||||||
|
if (!aggregatedText.isEmpty()) {
|
||||||
|
m_dataBuffers[requestId].responseContent = aggregatedText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message->handleStatus(statusStr);
|
||||||
|
} else {
|
||||||
|
message->handleStatus("incomplete");
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessageComplete(requestId);
|
||||||
|
} else if (!eventType.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("WARNING: OpenAIResponses - Unhandled event type '%1' for request %2\nData: %3")
|
||||||
|
.arg(eventType)
|
||||||
|
.arg(requestId)
|
||||||
|
.arg(QString::fromUtf8(QJsonDocument(data).toJson(QJsonDocument::Compact))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::emitPendingThinkingBlocks(const QString &requestId)
|
||||||
|
{
|
||||||
|
if (!m_messages.contains(requestId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||||
|
const auto thinkingBlocks = message->getCurrentThinkingContent();
|
||||||
|
|
||||||
|
if (thinkingBlocks.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int alreadyEmitted = m_emittedThinkingBlocksCount.value(requestId, 0);
|
||||||
|
const int totalBlocks = thinkingBlocks.size();
|
||||||
|
|
||||||
|
for (int i = alreadyEmitted; i < totalBlocks; ++i) {
|
||||||
|
const auto *thinkingContent = thinkingBlocks[i];
|
||||||
|
|
||||||
|
if (thinkingContent->thinking().trimmed().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit thinkingBlockReceived(
|
||||||
|
requestId, thinkingContent->thinking(), thinkingContent->signature());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_emittedThinkingBlocksCount[requestId] = totalBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::handleMessageComplete(const QString &requestId)
|
||||||
|
{
|
||||||
|
if (!m_messages.contains(requestId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||||
|
|
||||||
|
emitPendingThinkingBlocks(requestId);
|
||||||
|
|
||||||
|
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
|
||||||
|
const auto toolUseContent = message->getCurrentToolUseContent();
|
||||||
|
|
||||||
|
if (toolUseContent.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto *toolContent : toolUseContent) {
|
||||||
|
const auto toolStringName = m_toolsManager->toolsFactory()->getStringName(
|
||||||
|
toolContent->name());
|
||||||
|
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
|
||||||
|
m_toolsManager->executeToolCall(
|
||||||
|
requestId, toolContent->id(), toolContent->name(), toolContent->input());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::onToolExecutionComplete(
|
||||||
|
const QString &requestId, const QHash<QString, QString> &toolResults)
|
||||||
|
{
|
||||||
|
if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(QString("ERROR: OpenAIResponses - Missing data for continuation request %1")
|
||||||
|
.arg(requestId));
|
||||||
|
cleanupRequest(requestId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenAIResponsesMessage *message = m_messages[requestId];
|
||||||
|
const auto toolContent = message->getCurrentToolUseContent();
|
||||||
|
|
||||||
|
for (auto it = toolResults.constBegin(); it != toolResults.constEnd(); ++it) {
|
||||||
|
for (const auto *tool : toolContent) {
|
||||||
|
if (tool->id() == it.key()) {
|
||||||
|
const auto toolStringName = m_toolsManager->toolsFactory()->getStringName(
|
||||||
|
tool->name());
|
||||||
|
emit toolExecutionCompleted(
|
||||||
|
requestId, tool->id(), toolStringName, toolResults[tool->id()]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject continuationRequest = m_originalRequests[requestId];
|
||||||
|
QJsonArray input = continuationRequest["input"].toArray();
|
||||||
|
|
||||||
|
const QList<QJsonObject> assistantItems = message->toItemsFormat();
|
||||||
|
for (const QJsonObject &item : assistantItems) {
|
||||||
|
input.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray toolResultItems = message->createToolResultItems(toolResults);
|
||||||
|
for (const QJsonValue &item : toolResultItems) {
|
||||||
|
input.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
continuationRequest["input"] = input;
|
||||||
|
|
||||||
|
m_dataBuffers[requestId].responseContent.clear();
|
||||||
|
|
||||||
|
sendRequest(requestId, m_requestUrls[requestId], continuationRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAIResponsesProvider::cleanupRequest(const LLMCore::RequestID &requestId)
|
||||||
|
{
|
||||||
|
if (m_messages.contains(requestId)) {
|
||||||
|
OpenAIResponsesMessage *message = m_messages.take(requestId);
|
||||||
|
message->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dataBuffers.remove(requestId);
|
||||||
|
m_requestUrls.remove(requestId);
|
||||||
|
m_originalRequests.remove(requestId);
|
||||||
|
m_itemIdToCallId.remove(requestId);
|
||||||
|
m_emittedThinkingBlocksCount.remove(requestId);
|
||||||
|
m_toolsManager->cleanupRequest(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
86
providers/OpenAIResponsesProvider.hpp
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OpenAIResponsesMessage.hpp"
|
||||||
|
#include "tools/ToolsManager.hpp"
|
||||||
|
#include <llmcore/Provider.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Providers {
|
||||||
|
|
||||||
|
class OpenAIResponsesProvider : public LLMCore::Provider
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit OpenAIResponsesProvider(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString name() const override;
|
||||||
|
QString url() const override;
|
||||||
|
QString completionEndpoint() const override;
|
||||||
|
QString chatEndpoint() const override;
|
||||||
|
bool supportsModelListing() const override;
|
||||||
|
void prepareRequest(
|
||||||
|
QJsonObject &request,
|
||||||
|
LLMCore::PromptTemplate *prompt,
|
||||||
|
LLMCore::ContextData context,
|
||||||
|
LLMCore::RequestType type,
|
||||||
|
bool isToolsEnabled,
|
||||||
|
bool isThinkingEnabled) override;
|
||||||
|
QFuture<QList<QString>> getInstalledModels(const QString &url) override;
|
||||||
|
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
|
||||||
|
QString apiKey() const override;
|
||||||
|
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||||
|
LLMCore::ProviderID providerID() const override;
|
||||||
|
|
||||||
|
void sendRequest(
|
||||||
|
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override;
|
||||||
|
|
||||||
|
bool supportsTools() const override;
|
||||||
|
bool supportImage() const override;
|
||||||
|
bool supportThinking() const override;
|
||||||
|
void cancelRequest(const LLMCore::RequestID &requestId) override;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void onDataReceived(
|
||||||
|
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||||
|
void onRequestFinished(
|
||||||
|
const QodeAssist::LLMCore::RequestID &requestId,
|
||||||
|
std::optional<QString> error) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onToolExecutionComplete(
|
||||||
|
const QString &requestId, const QHash<QString, QString> &toolResults);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void processStreamEvent(const QString &requestId, const QString &eventType, const QJsonObject &data);
|
||||||
|
void emitPendingThinkingBlocks(const QString &requestId);
|
||||||
|
void handleMessageComplete(const QString &requestId);
|
||||||
|
void cleanupRequest(const LLMCore::RequestID &requestId);
|
||||||
|
|
||||||
|
QHash<LLMCore::RequestID, OpenAIResponsesMessage *> m_messages;
|
||||||
|
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
|
||||||
|
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
|
||||||
|
QHash<LLMCore::RequestID, QHash<QString, QString>> m_itemIdToCallId;
|
||||||
|
QHash<LLMCore::RequestID, int> m_emittedThinkingBlocksCount;
|
||||||
|
Tools::ToolsManager *m_toolsManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Providers
|
||||||
|
|
||||||
255
providers/OpenAIResponsesRequestBuilder.hpp
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "OpenAIResponses/ModelRequest.hpp"
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::OpenAIResponses {
|
||||||
|
|
||||||
|
class RequestBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RequestBuilder() = default;
|
||||||
|
|
||||||
|
RequestBuilder &setModel(QString model)
|
||||||
|
{
|
||||||
|
m_model = std::move(model);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &addMessage(Role role, QString content)
|
||||||
|
{
|
||||||
|
Message msg;
|
||||||
|
msg.role = role;
|
||||||
|
msg.content.append(MessageContent(std::move(content)));
|
||||||
|
m_messages.append(std::move(msg));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &addMessage(Message msg)
|
||||||
|
{
|
||||||
|
m_messages.append(std::move(msg));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setInstructions(QString instructions)
|
||||||
|
{
|
||||||
|
m_instructions = std::move(instructions);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &addTool(Tool tool)
|
||||||
|
{
|
||||||
|
m_tools.append(std::move(tool));
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setTemperature(double temp) noexcept
|
||||||
|
{
|
||||||
|
m_temperature = temp;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setTopP(double topP) noexcept
|
||||||
|
{
|
||||||
|
m_topP = topP;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setMaxOutputTokens(int tokens) noexcept
|
||||||
|
{
|
||||||
|
m_maxOutputTokens = tokens;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setStream(bool stream) noexcept
|
||||||
|
{
|
||||||
|
m_stream = stream;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setStore(bool store) noexcept
|
||||||
|
{
|
||||||
|
m_store = store;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setTextFormat(TextFormatOptions format)
|
||||||
|
{
|
||||||
|
m_textFormat = std::move(format);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setReasoningEffort(ReasoningEffort effort) noexcept
|
||||||
|
{
|
||||||
|
m_reasoningEffort = effort;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setMetadata(QMap<QString, QVariant> metadata)
|
||||||
|
{
|
||||||
|
m_metadata = std::move(metadata);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &setIncludeReasoningContent(bool include) noexcept
|
||||||
|
{
|
||||||
|
m_includeReasoningContent = include;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestBuilder &clear() noexcept
|
||||||
|
{
|
||||||
|
m_model.clear();
|
||||||
|
m_messages.clear();
|
||||||
|
m_instructions.reset();
|
||||||
|
m_tools.clear();
|
||||||
|
m_temperature.reset();
|
||||||
|
m_topP.reset();
|
||||||
|
m_maxOutputTokens.reset();
|
||||||
|
m_stream = false;
|
||||||
|
m_store.reset();
|
||||||
|
m_textFormat.reset();
|
||||||
|
m_reasoningEffort.reset();
|
||||||
|
m_includeReasoningContent = false;
|
||||||
|
m_metadata.clear();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
QJsonObject obj;
|
||||||
|
|
||||||
|
if (!m_model.isEmpty()) {
|
||||||
|
obj["model"] = m_model;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_messages.isEmpty()) {
|
||||||
|
if (m_messages.size() == 1 && m_messages[0].role == Role::User
|
||||||
|
&& m_messages[0].content.size() == 1) {
|
||||||
|
obj["input"] = m_messages[0].content[0].toJson();
|
||||||
|
} else {
|
||||||
|
QJsonArray input;
|
||||||
|
for (const auto &msg : m_messages) {
|
||||||
|
input.append(msg.toJson());
|
||||||
|
}
|
||||||
|
obj["input"] = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_instructions) {
|
||||||
|
obj["instructions"] = *m_instructions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_tools.isEmpty()) {
|
||||||
|
QJsonArray tools;
|
||||||
|
for (const auto &tool : m_tools) {
|
||||||
|
tools.append(tool.toJson());
|
||||||
|
}
|
||||||
|
obj["tools"] = tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_temperature) {
|
||||||
|
obj["temperature"] = *m_temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_topP) {
|
||||||
|
obj["top_p"] = *m_topP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_maxOutputTokens) {
|
||||||
|
obj["max_output_tokens"] = *m_maxOutputTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj["stream"] = m_stream;
|
||||||
|
|
||||||
|
if (m_store) {
|
||||||
|
obj["store"] = *m_store;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_textFormat) {
|
||||||
|
QJsonObject textObj;
|
||||||
|
textObj["format"] = m_textFormat->toJson();
|
||||||
|
obj["text"] = textObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_reasoningEffort) {
|
||||||
|
QJsonObject reasoning;
|
||||||
|
reasoning["effort"] = effortToString(*m_reasoningEffort);
|
||||||
|
obj["reasoning"] = reasoning;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_includeReasoningContent) {
|
||||||
|
QJsonArray include;
|
||||||
|
include.append("reasoning.encrypted_content");
|
||||||
|
obj["include"] = include;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_metadata.isEmpty()) {
|
||||||
|
QJsonObject metadata;
|
||||||
|
for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
|
||||||
|
metadata[it.key()] = QJsonValue::fromVariant(it.value());
|
||||||
|
}
|
||||||
|
obj["metadata"] = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_model;
|
||||||
|
QList<Message> m_messages;
|
||||||
|
std::optional<QString> m_instructions;
|
||||||
|
QList<Tool> m_tools;
|
||||||
|
std::optional<double> m_temperature;
|
||||||
|
std::optional<double> m_topP;
|
||||||
|
std::optional<int> m_maxOutputTokens;
|
||||||
|
bool m_stream = false;
|
||||||
|
std::optional<bool> m_store;
|
||||||
|
std::optional<TextFormatOptions> m_textFormat;
|
||||||
|
std::optional<ReasoningEffort> m_reasoningEffort;
|
||||||
|
bool m_includeReasoningContent = false;
|
||||||
|
QMap<QString, QVariant> m_metadata;
|
||||||
|
|
||||||
|
static QString effortToString(ReasoningEffort e)
|
||||||
|
{
|
||||||
|
switch (e) {
|
||||||
|
case ReasoningEffort::None:
|
||||||
|
return "none";
|
||||||
|
case ReasoningEffort::Minimal:
|
||||||
|
return "minimal";
|
||||||
|
case ReasoningEffort::Low:
|
||||||
|
return "low";
|
||||||
|
case ReasoningEffort::Medium:
|
||||||
|
return "medium";
|
||||||
|
case ReasoningEffort::High:
|
||||||
|
return "high";
|
||||||
|
}
|
||||||
|
return "medium";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::OpenAIResponses
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "providers/OllamaProvider.hpp"
|
#include "providers/OllamaProvider.hpp"
|
||||||
#include "providers/OpenAICompatProvider.hpp"
|
#include "providers/OpenAICompatProvider.hpp"
|
||||||
#include "providers/OpenAIProvider.hpp"
|
#include "providers/OpenAIProvider.hpp"
|
||||||
|
#include "providers/OpenAIResponsesProvider.hpp"
|
||||||
#include "providers/OpenRouterAIProvider.hpp"
|
#include "providers/OpenRouterAIProvider.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Providers {
|
namespace QodeAssist::Providers {
|
||||||
@@ -39,6 +40,7 @@ inline void registerProviders()
|
|||||||
providerManager.registerProvider<OllamaProvider>();
|
providerManager.registerProvider<OllamaProvider>();
|
||||||
providerManager.registerProvider<ClaudeProvider>();
|
providerManager.registerProvider<ClaudeProvider>();
|
||||||
providerManager.registerProvider<OpenAIProvider>();
|
providerManager.registerProvider<OpenAIProvider>();
|
||||||
|
providerManager.registerProvider<OpenAIResponsesProvider>();
|
||||||
providerManager.registerProvider<OpenAICompatProvider>();
|
providerManager.registerProvider<OpenAICompatProvider>();
|
||||||
providerManager.registerProvider<LMStudioProvider>();
|
providerManager.registerProvider<LMStudioProvider>();
|
||||||
providerManager.registerProvider<OpenRouterProvider>();
|
providerManager.registerProvider<OpenRouterProvider>();
|
||||||
|
|||||||
@@ -57,11 +57,13 @@
|
|||||||
#include "settings/ChatAssistantSettings.hpp"
|
#include "settings/ChatAssistantSettings.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
#include "settings/ProjectSettingsPanel.hpp"
|
#include "settings/ProjectSettingsPanel.hpp"
|
||||||
|
#include "settings/QuickRefactorSettings.hpp"
|
||||||
#include "settings/SettingsConstants.hpp"
|
#include "settings/SettingsConstants.hpp"
|
||||||
#include "templates/Templates.hpp"
|
#include "templates/Templates.hpp"
|
||||||
#include "widgets/CustomInstructionsManager.hpp"
|
#include "widgets/CustomInstructionsManager.hpp"
|
||||||
#include "widgets/QuickRefactorDialog.hpp"
|
#include "widgets/QuickRefactorDialog.hpp"
|
||||||
#include <ChatView/ChatView.hpp>
|
#include <ChatView/ChatView.hpp>
|
||||||
|
#include <ChatView/ChatFileManager.hpp>
|
||||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||||
#include <coreplugin/actionmanager/actionmanager.h>
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
@@ -87,6 +89,8 @@ public:
|
|||||||
|
|
||||||
~QodeAssistPlugin() final
|
~QodeAssistPlugin() final
|
||||||
{
|
{
|
||||||
|
Chat::ChatFileManager::cleanupGlobalIntermediateStorage();
|
||||||
|
|
||||||
delete m_qodeAssistClient;
|
delete m_qodeAssistClient;
|
||||||
if (m_chatOutputPane) {
|
if (m_chatOutputPane) {
|
||||||
delete m_chatOutputPane;
|
delete m_chatOutputPane;
|
||||||
@@ -232,7 +236,7 @@ public:
|
|||||||
closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat"));
|
closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat"));
|
||||||
closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
|
closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
|
||||||
closeChatViewAction.addOnTriggered(this, [this] {
|
closeChatViewAction.addOnTriggered(this, [this] {
|
||||||
if (m_chatView->isVisible()) {
|
if (m_chatView && m_chatView->isActive() && m_chatView->isVisible()) {
|
||||||
m_chatView->close();
|
m_chatView->close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -246,9 +250,9 @@ public:
|
|||||||
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
|
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
|
||||||
editorContextMenu->addAction(showChatViewAction.command(),
|
editorContextMenu->addAction(showChatViewAction.command(),
|
||||||
Core::Constants::G_DEFAULT_THREE);
|
Core::Constants::G_DEFAULT_THREE);
|
||||||
editorContextMenu->addAction(closeChatViewAction.command(),
|
|
||||||
Core::Constants::G_DEFAULT_THREE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Chat::ChatFileManager::cleanupGlobalIntermediateStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
void extensionsInitialized() final {}
|
void extensionsInitialized() final {}
|
||||||
|
|||||||
226
settings/AgentRole.cpp
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AgentRole.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
QString AgentRolesManager::getConfigurationDirectory()
|
||||||
|
{
|
||||||
|
QString path = QString("%1/qodeassist/agent_roles")
|
||||||
|
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||||
|
QDir().mkpath(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<AgentRole> AgentRolesManager::loadAllRoles()
|
||||||
|
{
|
||||||
|
QList<AgentRole> roles;
|
||||||
|
QString configDir = getConfigurationDirectory();
|
||||||
|
QDir dir(configDir);
|
||||||
|
|
||||||
|
ensureDefaultRoles();
|
||||||
|
|
||||||
|
const QStringList jsonFiles = dir.entryList({"*.json"}, QDir::Files);
|
||||||
|
for (const QString &fileName : jsonFiles) {
|
||||||
|
AgentRole role = loadRoleFromFile(dir.absoluteFilePath(fileName));
|
||||||
|
if (!role.id.isEmpty()) {
|
||||||
|
roles.append(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRole AgentRolesManager::loadRole(const QString &roleId)
|
||||||
|
{
|
||||||
|
if (roleId.isEmpty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||||
|
if (!QFile::exists(filePath))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return loadRoleFromFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRole AgentRolesManager::loadRoleFromFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||||
|
if (doc.isNull() || !doc.isObject())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return AgentRole::fromJson(doc.object());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AgentRolesManager::saveRole(const AgentRole &role)
|
||||||
|
{
|
||||||
|
if (role.id.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(role.id + ".json");
|
||||||
|
QFile file(filePath);
|
||||||
|
|
||||||
|
if (!file.open(QIODevice::WriteOnly))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
QJsonDocument doc(role.toJson());
|
||||||
|
file.write(doc.toJson(QJsonDocument::Indented));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AgentRolesManager::deleteRole(const QString &roleId)
|
||||||
|
{
|
||||||
|
if (roleId.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||||
|
return QFile::remove(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AgentRolesManager::roleExists(const QString &roleId)
|
||||||
|
{
|
||||||
|
if (roleId.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
|
||||||
|
return QFile::exists(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesManager::ensureDefaultRoles()
|
||||||
|
{
|
||||||
|
QDir dir(getConfigurationDirectory());
|
||||||
|
|
||||||
|
if (!dir.exists("developer.json"))
|
||||||
|
saveRole(getDefaultDeveloperRole());
|
||||||
|
|
||||||
|
if (!dir.exists("reviewer.json"))
|
||||||
|
saveRole(getDefaultReviewerRole());
|
||||||
|
|
||||||
|
if (!dir.exists("researcher.json"))
|
||||||
|
saveRole(getDefaultResearcherRole());
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRole AgentRolesManager::getDefaultDeveloperRole()
|
||||||
|
{
|
||||||
|
return AgentRole{
|
||||||
|
"developer",
|
||||||
|
"Developer",
|
||||||
|
"Experienced Qt/C++ developer for implementation tasks",
|
||||||
|
"You are an experienced Qt/C++ developer working on a Qt Creator plugin.\n\n"
|
||||||
|
"Your workflow:\n"
|
||||||
|
"1. **Analyze** - understand the problem and what needs to be done\n"
|
||||||
|
"2. **Propose solution** - explain your approach in 2-3 sentences\n"
|
||||||
|
"3. **Wait for approval** - don't write code until the solution is confirmed\n"
|
||||||
|
"4. **Implement** - write clean, minimal code that solves the task\n\n"
|
||||||
|
"When analyzing:\n"
|
||||||
|
"- Ask clarifying questions if requirements are unclear\n"
|
||||||
|
"- Check existing code for similar patterns\n"
|
||||||
|
"- Consider edge cases and potential issues\n\n"
|
||||||
|
"When proposing:\n"
|
||||||
|
"- Explain what you'll change and why\n"
|
||||||
|
"- Mention files you'll modify\n"
|
||||||
|
"- Note any architectural implications\n\n"
|
||||||
|
"When implementing:\n"
|
||||||
|
"- Use C++20, Qt6, follow existing codebase style\n"
|
||||||
|
"- Write only what's needed (MVP approach)\n"
|
||||||
|
"- Include file paths and necessary changes\n"
|
||||||
|
"- Handle errors properly\n"
|
||||||
|
"- Make sure it compiles\n\n"
|
||||||
|
"Keep it practical:\n"
|
||||||
|
"- Short explanations, let code speak\n"
|
||||||
|
"- No over-engineering or unnecessary refactoring\n"
|
||||||
|
"- No TODOs, debug code, or unfinished work\n"
|
||||||
|
"- Point out non-obvious things\n\n"
|
||||||
|
"You're a pragmatic team member who thinks before coding.",
|
||||||
|
true};
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRole AgentRolesManager::getDefaultReviewerRole()
|
||||||
|
{
|
||||||
|
return AgentRole{
|
||||||
|
"reviewer",
|
||||||
|
"Code Reviewer",
|
||||||
|
"Expert C++/QML code reviewer for quality assurance",
|
||||||
|
"You are an expert C++/QML code reviewer specializing in C++20 and Qt6.\n\n"
|
||||||
|
"What you check:\n"
|
||||||
|
"- Bugs, memory leaks, undefined behavior\n"
|
||||||
|
"- C++20 compliance and Qt6 patterns\n"
|
||||||
|
"- RAII, move semantics, smart pointers\n"
|
||||||
|
"- Qt parent-child ownership and signal/slot correctness\n"
|
||||||
|
"- Thread safety and Qt concurrent usage\n"
|
||||||
|
"- const-correctness and Qt container usage\n"
|
||||||
|
"- Performance bottlenecks\n"
|
||||||
|
"- Production readiness: error handling, no debug leftovers\n\n"
|
||||||
|
"What you do:\n"
|
||||||
|
"- Point out problems with clear explanations\n"
|
||||||
|
"- Suggest specific fixes with code examples\n"
|
||||||
|
"- Remove unnecessary comments, keep essential docs only\n"
|
||||||
|
"- Flag anything that's not production-ready\n"
|
||||||
|
"- Recommend optimizations when you spot them\n\n"
|
||||||
|
"Focus on: correctness, performance, maintainability, Qt idioms.\n\n"
|
||||||
|
"Be direct and specific. Show, don't just tell.",
|
||||||
|
true};
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRole AgentRolesManager::getDefaultResearcherRole()
|
||||||
|
{
|
||||||
|
return AgentRole{
|
||||||
|
"researcher",
|
||||||
|
"Researcher",
|
||||||
|
"Research-oriented developer for exploring solutions",
|
||||||
|
"You are a research-oriented Qt/C++ developer who investigates problems and explores "
|
||||||
|
"solutions.\n\n"
|
||||||
|
"Your job is to think, not to code:\n"
|
||||||
|
"- Deep dive into the problem before suggesting anything\n"
|
||||||
|
"- Research Qt docs, patterns, and best practices\n"
|
||||||
|
"- Find multiple ways to solve it\n"
|
||||||
|
"- Compare trade-offs: performance, complexity, maintainability\n"
|
||||||
|
"- Look for relevant Qt APIs and modules\n"
|
||||||
|
"- Think about architectural consequences\n\n"
|
||||||
|
"How you work:\n"
|
||||||
|
"1. **Problem Analysis** - what exactly needs solving\n"
|
||||||
|
"2. **Research Findings** - what you learned about this problem space\n"
|
||||||
|
"3. **Solution Options** - present 2-3 approaches with honest pros/cons\n"
|
||||||
|
"4. **Recommendation** - which one fits best and why\n"
|
||||||
|
"5. **Next Steps** - what to consider before implementing\n\n"
|
||||||
|
"What you provide:\n"
|
||||||
|
"- Clear comparison of different approaches\n"
|
||||||
|
"- Code snippets as examples (not ready-to-use patches)\n"
|
||||||
|
"- Links to docs, examples, similar implementations\n"
|
||||||
|
"- Questions to clarify requirements\n"
|
||||||
|
"- Warning about potential problems\n\n"
|
||||||
|
"You DO NOT write implementation code. You explore options and let the developer choose.\n\n"
|
||||||
|
"Think like a consultant: research thoroughly, present clearly, stay objective.",
|
||||||
|
true};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
82
settings/AgentRole.hpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
struct AgentRole
|
||||||
|
{
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QString description;
|
||||||
|
QString systemPrompt;
|
||||||
|
bool isBuiltin = false;
|
||||||
|
|
||||||
|
QJsonObject toJson() const
|
||||||
|
{
|
||||||
|
return QJsonObject{
|
||||||
|
{"id", id},
|
||||||
|
{"name", name},
|
||||||
|
{"description", description},
|
||||||
|
{"systemPrompt", systemPrompt},
|
||||||
|
{"isBuiltin", isBuiltin}};
|
||||||
|
}
|
||||||
|
|
||||||
|
static AgentRole fromJson(const QJsonObject &json)
|
||||||
|
{
|
||||||
|
return AgentRole{
|
||||||
|
json["id"].toString(),
|
||||||
|
json["name"].toString(),
|
||||||
|
json["description"].toString(),
|
||||||
|
json["systemPrompt"].toString(),
|
||||||
|
json["isBuiltin"].toBool(false)};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const AgentRole &other) const { return id == other.id; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class AgentRolesManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static QString getConfigurationDirectory();
|
||||||
|
static QList<AgentRole> loadAllRoles();
|
||||||
|
static AgentRole loadRole(const QString &roleId);
|
||||||
|
static AgentRole loadRoleFromFile(const QString &filePath);
|
||||||
|
static bool saveRole(const AgentRole &role);
|
||||||
|
static bool deleteRole(const QString &roleId);
|
||||||
|
static bool roleExists(const QString &roleId);
|
||||||
|
static void ensureDefaultRoles();
|
||||||
|
|
||||||
|
static AgentRole getNoRole()
|
||||||
|
{
|
||||||
|
return AgentRole{"", "No Role", "Use base system prompt without role specialization", "", false};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static AgentRole getDefaultDeveloperRole();
|
||||||
|
static AgentRole getDefaultReviewerRole();
|
||||||
|
static AgentRole getDefaultResearcherRole();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
125
settings/AgentRoleDialog.cpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AgentRoleDialog.hpp"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
AgentRoleDialog::AgentRoleDialog(Action action, QWidget *parent)
|
||||||
|
: QDialog{parent}
|
||||||
|
, m_action{action}
|
||||||
|
{
|
||||||
|
auto getTitle = [](Action action) {
|
||||||
|
switch(action)
|
||||||
|
{
|
||||||
|
case Action::Add:
|
||||||
|
return tr("Add Agent Role");
|
||||||
|
case Action::Duplicate:
|
||||||
|
return tr("Duplicate Agent Role");
|
||||||
|
case Action::Edit:
|
||||||
|
return tr("Edit Agent Role");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setWindowTitle(getTitle(action));
|
||||||
|
setupUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRoleDialog::setupUI()
|
||||||
|
{
|
||||||
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
|
auto *formLayout = new QFormLayout();
|
||||||
|
|
||||||
|
m_nameEdit = new QLineEdit(this);
|
||||||
|
m_nameEdit->setPlaceholderText(tr("e.g., Developer, Code Reviewer"));
|
||||||
|
formLayout->addRow(tr("Name:"), m_nameEdit);
|
||||||
|
|
||||||
|
m_idEdit = new QLineEdit(this);
|
||||||
|
m_idEdit->setPlaceholderText(tr("e.g., developer, code_reviewer"));
|
||||||
|
formLayout->addRow(tr("ID:"), m_idEdit);
|
||||||
|
|
||||||
|
m_descriptionEdit = new QTextEdit(this);
|
||||||
|
m_descriptionEdit->setPlaceholderText(tr("Brief description of this role..."));
|
||||||
|
m_descriptionEdit->setMaximumHeight(80);
|
||||||
|
formLayout->addRow(tr("Description:"), m_descriptionEdit);
|
||||||
|
|
||||||
|
mainLayout->addLayout(formLayout);
|
||||||
|
|
||||||
|
auto *promptLabel = new QLabel(tr("System Prompt:"), this);
|
||||||
|
mainLayout->addWidget(promptLabel);
|
||||||
|
|
||||||
|
m_systemPromptEdit = new QTextEdit(this);
|
||||||
|
m_systemPromptEdit->setPlaceholderText(
|
||||||
|
tr("You are an expert in...\n\nYour role is to:\n- Task 1\n- Task 2\n- Task 3"));
|
||||||
|
mainLayout->addWidget(m_systemPromptEdit);
|
||||||
|
|
||||||
|
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||||
|
mainLayout->addWidget(m_buttonBox);
|
||||||
|
|
||||||
|
connect(m_buttonBox, &QDialogButtonBox::accepted, this, &AgentRoleDialog::accept);
|
||||||
|
connect(m_buttonBox, &QDialogButtonBox::rejected, this, &AgentRoleDialog::reject);
|
||||||
|
connect(m_nameEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||||
|
connect(m_idEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||||
|
connect(m_systemPromptEdit, &QTextEdit::textChanged, this, &AgentRoleDialog::validateInput);
|
||||||
|
|
||||||
|
if (m_action == Action::Edit) {
|
||||||
|
m_idEdit->setEnabled(false);
|
||||||
|
m_idEdit->setToolTip(tr("ID cannot be changed for existing roles"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setMinimumSize(600, 500);
|
||||||
|
validateInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRoleDialog::validateInput()
|
||||||
|
{
|
||||||
|
bool valid = !m_nameEdit->text().trimmed().isEmpty()
|
||||||
|
&& !m_idEdit->text().trimmed().isEmpty()
|
||||||
|
&& !m_systemPromptEdit->toPlainText().trimmed().isEmpty();
|
||||||
|
|
||||||
|
m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRole AgentRoleDialog::getRole() const
|
||||||
|
{
|
||||||
|
return AgentRole{
|
||||||
|
m_idEdit->text().trimmed(),
|
||||||
|
m_nameEdit->text().trimmed(),
|
||||||
|
m_descriptionEdit->toPlainText().trimmed(),
|
||||||
|
m_systemPromptEdit->toPlainText().trimmed(),
|
||||||
|
false};
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRoleDialog::setRole(const AgentRole &role)
|
||||||
|
{
|
||||||
|
m_idEdit->setText(role.id);
|
||||||
|
m_nameEdit->setText(role.name);
|
||||||
|
m_descriptionEdit->setPlainText(role.description);
|
||||||
|
m_systemPromptEdit->setPlainText(role.systemPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
65
settings/AgentRoleDialog.hpp
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
#include "AgentRole.hpp"
|
||||||
|
|
||||||
|
class QLineEdit;
|
||||||
|
class QTextEdit;
|
||||||
|
class QDialogButtonBox;
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
class AgentRoleDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum class Action {
|
||||||
|
Add,
|
||||||
|
Duplicate,
|
||||||
|
Edit,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit AgentRoleDialog(Action action, QWidget *parent = nullptr);
|
||||||
|
explicit AgentRoleDialog(const AgentRole &role, Action action, QWidget *parent = nullptr)
|
||||||
|
: AgentRoleDialog{action, parent}
|
||||||
|
{
|
||||||
|
setRole(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRole getRole() const;
|
||||||
|
void setRole(const AgentRole &role);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupUI();
|
||||||
|
void validateInput();
|
||||||
|
|
||||||
|
QLineEdit *m_nameEdit = nullptr;
|
||||||
|
QLineEdit *m_idEdit = nullptr;
|
||||||
|
QTextEdit *m_descriptionEdit = nullptr;
|
||||||
|
QTextEdit *m_systemPromptEdit = nullptr;
|
||||||
|
QDialogButtonBox *m_buttonBox = nullptr;
|
||||||
|
Action m_action;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
257
settings/AgentRolesWidget.cpp
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AgentRolesWidget.hpp"
|
||||||
|
|
||||||
|
#include "AgentRole.hpp"
|
||||||
|
#include "AgentRoleDialog.hpp"
|
||||||
|
#include "SettingsTr.hpp"
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
void AgentRolesWidget::setupUI()
|
||||||
|
{
|
||||||
|
auto *mainLayout = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
auto *headerLayout = new QHBoxLayout();
|
||||||
|
|
||||||
|
auto *infoLabel = new QLabel(
|
||||||
|
Tr::tr("Agent roles define different system prompts for specific tasks."), this);
|
||||||
|
infoLabel->setWordWrap(true);
|
||||||
|
headerLayout->addWidget(infoLabel, 1);
|
||||||
|
|
||||||
|
auto *openFolderButton = new QPushButton(Tr::tr("Open Roles Folder..."), this);
|
||||||
|
connect(openFolderButton, &QPushButton::clicked, this, &AgentRolesWidget::onOpenRolesFolder);
|
||||||
|
headerLayout->addWidget(openFolderButton);
|
||||||
|
|
||||||
|
mainLayout->addLayout(headerLayout);
|
||||||
|
|
||||||
|
auto *contentLayout = new QHBoxLayout();
|
||||||
|
|
||||||
|
m_rolesList = new QListWidget(this);
|
||||||
|
m_rolesList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
connect(m_rolesList, &QListWidget::itemSelectionChanged, this, &AgentRolesWidget::updateButtons);
|
||||||
|
connect(m_rolesList, &QListWidget::itemDoubleClicked, this, &AgentRolesWidget::onEditRole);
|
||||||
|
contentLayout->addWidget(m_rolesList, 1);
|
||||||
|
|
||||||
|
auto *buttonsLayout = new QVBoxLayout();
|
||||||
|
|
||||||
|
m_addButton = new QPushButton(Tr::tr("Add..."), this);
|
||||||
|
connect(m_addButton, &QPushButton::clicked, this, &AgentRolesWidget::onAddRole);
|
||||||
|
buttonsLayout->addWidget(m_addButton);
|
||||||
|
|
||||||
|
m_editButton = new QPushButton(Tr::tr("Edit..."), this);
|
||||||
|
connect(m_editButton, &QPushButton::clicked, this, &AgentRolesWidget::onEditRole);
|
||||||
|
buttonsLayout->addWidget(m_editButton);
|
||||||
|
|
||||||
|
m_duplicateButton = new QPushButton(Tr::tr("Duplicate..."), this);
|
||||||
|
connect(m_duplicateButton, &QPushButton::clicked, this, &AgentRolesWidget::onDuplicateRole);
|
||||||
|
buttonsLayout->addWidget(m_duplicateButton);
|
||||||
|
|
||||||
|
m_deleteButton = new QPushButton(Tr::tr("Delete"), this);
|
||||||
|
connect(m_deleteButton, &QPushButton::clicked, this, &AgentRolesWidget::onDeleteRole);
|
||||||
|
buttonsLayout->addWidget(m_deleteButton);
|
||||||
|
|
||||||
|
buttonsLayout->addStretch();
|
||||||
|
|
||||||
|
contentLayout->addLayout(buttonsLayout);
|
||||||
|
mainLayout->addLayout(contentLayout);
|
||||||
|
|
||||||
|
updateButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesWidget::loadRoles()
|
||||||
|
{
|
||||||
|
m_rolesList->clear();
|
||||||
|
|
||||||
|
const QList<AgentRole> roles = AgentRolesManager::loadAllRoles();
|
||||||
|
for (const AgentRole &role : roles) {
|
||||||
|
auto *item = new QListWidgetItem(role.name, m_rolesList);
|
||||||
|
item->setData(Qt::UserRole, role.id);
|
||||||
|
|
||||||
|
QString tooltip = role.description;
|
||||||
|
if (role.isBuiltin) {
|
||||||
|
tooltip += "\n\n" + Tr::tr("(Built-in role)");
|
||||||
|
item->setForeground(Qt::darkGray);
|
||||||
|
}
|
||||||
|
item->setToolTip(tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesWidget::updateButtons()
|
||||||
|
{
|
||||||
|
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||||
|
bool hasSelection = selectedItem != nullptr;
|
||||||
|
bool isBuiltin = false;
|
||||||
|
|
||||||
|
if (hasSelection) {
|
||||||
|
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||||
|
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||||
|
isBuiltin = role.isBuiltin;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_editButton->setEnabled(hasSelection);
|
||||||
|
m_duplicateButton->setEnabled(hasSelection);
|
||||||
|
m_deleteButton->setEnabled(hasSelection && !isBuiltin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesWidget::onAddRole()
|
||||||
|
{
|
||||||
|
AgentRoleDialog dialog{AgentRoleDialog::Action::Add, this};
|
||||||
|
if (dialog.exec() != QDialog::Accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AgentRole newRole = dialog.getRole();
|
||||||
|
|
||||||
|
if (AgentRolesManager::roleExists(newRole.id)) {
|
||||||
|
QMessageBox::warning(
|
||||||
|
this,
|
||||||
|
Tr::tr("Role Already Exists"),
|
||||||
|
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
|
||||||
|
.arg(newRole.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AgentRolesManager::saveRole(newRole)) {
|
||||||
|
loadRoles();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(
|
||||||
|
this, Tr::tr("Error"), Tr::tr("Failed to save role '%1'.").arg(newRole.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesWidget::onEditRole()
|
||||||
|
{
|
||||||
|
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||||
|
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||||
|
|
||||||
|
if (role.isBuiltin) {
|
||||||
|
QMessageBox::information(
|
||||||
|
this,
|
||||||
|
Tr::tr("Cannot Edit Built-in Role"),
|
||||||
|
Tr::tr(
|
||||||
|
"Built-in roles cannot be edited. You can duplicate this role and modify the copy."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRoleDialog dialog{role, AgentRoleDialog::Action::Edit, this};
|
||||||
|
if (dialog.exec() != QDialog::Accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AgentRole updatedRole = dialog.getRole();
|
||||||
|
|
||||||
|
if (AgentRolesManager::saveRole(updatedRole)) {
|
||||||
|
loadRoles();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(
|
||||||
|
this, Tr::tr("Error"), Tr::tr("Failed to update role '%1'.").arg(updatedRole.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesWidget::onDuplicateRole()
|
||||||
|
{
|
||||||
|
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||||
|
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||||
|
|
||||||
|
role.name += " (Copy)";
|
||||||
|
role.id += "_copy";
|
||||||
|
role.isBuiltin = false;
|
||||||
|
|
||||||
|
int counter = 1;
|
||||||
|
QString baseId = role.id;
|
||||||
|
while (AgentRolesManager::roleExists(role.id)) {
|
||||||
|
role.id = baseId + QString::number(counter++);
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentRoleDialog dialog{role, AgentRoleDialog::Action::Duplicate, this};
|
||||||
|
if (dialog.exec() != QDialog::Accepted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AgentRole newRole = dialog.getRole();
|
||||||
|
|
||||||
|
if (AgentRolesManager::roleExists(newRole.id)) {
|
||||||
|
QMessageBox::warning(
|
||||||
|
this,
|
||||||
|
Tr::tr("Role Already Exists"),
|
||||||
|
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
|
||||||
|
.arg(newRole.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AgentRolesManager::saveRole(newRole)) {
|
||||||
|
loadRoles();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(this, Tr::tr("Error"), Tr::tr("Failed to duplicate role."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesWidget::onDeleteRole()
|
||||||
|
{
|
||||||
|
QListWidgetItem *selectedItem = m_rolesList->currentItem();
|
||||||
|
if (!selectedItem)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString roleId = selectedItem->data(Qt::UserRole).toString();
|
||||||
|
AgentRole role = AgentRolesManager::loadRole(roleId);
|
||||||
|
|
||||||
|
if (role.isBuiltin) {
|
||||||
|
QMessageBox::information(
|
||||||
|
this, Tr::tr("Cannot Delete Built-in Role"), Tr::tr("Built-in roles cannot be deleted."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||||
|
this,
|
||||||
|
Tr::tr("Delete Role"),
|
||||||
|
Tr::tr("Are you sure you want to delete the role '%1'?").arg(role.name),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
|
||||||
|
if (reply == QMessageBox::Yes) {
|
||||||
|
if (AgentRolesManager::deleteRole(roleId)) {
|
||||||
|
loadRoles();
|
||||||
|
} else {
|
||||||
|
QMessageBox::critical(
|
||||||
|
this, Tr::tr("Error"), Tr::tr("Failed to delete role '%1'.").arg(role.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AgentRolesWidget::onOpenRolesFolder()
|
||||||
|
{
|
||||||
|
QDesktopServices::openUrl(QUrl::fromLocalFile(AgentRolesManager::getConfigurationDirectory()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
58
settings/AgentRolesWidget.hpp
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <coreplugin/dialogs/ioptionspage.h>
|
||||||
|
|
||||||
|
class QListWidget;
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
class AgentRolesWidget : public Core::IOptionsPageWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit AgentRolesWidget()
|
||||||
|
{
|
||||||
|
setupUI();
|
||||||
|
loadRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupUI();
|
||||||
|
void loadRoles();
|
||||||
|
void updateButtons();
|
||||||
|
|
||||||
|
void onAddRole();
|
||||||
|
void onEditRole();
|
||||||
|
void onDuplicateRole();
|
||||||
|
void onDeleteRole();
|
||||||
|
void onOpenRolesFolder();
|
||||||
|
|
||||||
|
QListWidget *m_rolesList = nullptr;
|
||||||
|
QPushButton *m_addButton = nullptr;
|
||||||
|
QPushButton *m_editButton = nullptr;
|
||||||
|
QPushButton *m_duplicateButton = nullptr;
|
||||||
|
QPushButton *m_deleteButton = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
@@ -16,11 +16,15 @@ add_library(QodeAssistSettings STATIC
|
|||||||
ProviderSettings.hpp ProviderSettings.cpp
|
ProviderSettings.hpp ProviderSettings.cpp
|
||||||
PluginUpdater.hpp PluginUpdater.cpp
|
PluginUpdater.hpp PluginUpdater.cpp
|
||||||
UpdateDialog.hpp UpdateDialog.cpp
|
UpdateDialog.hpp UpdateDialog.cpp
|
||||||
|
AgentRole.hpp AgentRole.cpp
|
||||||
|
AgentRoleDialog.hpp AgentRoleDialog.cpp
|
||||||
|
AgentRolesWidget.hpp AgentRolesWidget.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistSettings
|
target_link_libraries(QodeAssistSettings
|
||||||
PUBLIC
|
PUBLIC
|
||||||
Qt::Core
|
Qt::Core
|
||||||
|
Qt::Widgets
|
||||||
Qt::Network
|
Qt::Network
|
||||||
QtCreator::Core
|
QtCreator::Core
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
|
|||||||