Compare commits

..

1 Commits

Author SHA1 Message Date
Petr Mironychev
989063f6c8 feat: Add support QtCreator 19.0.1 2026-04-25 02:28:17 +02:00
575 changed files with 13770 additions and 69891 deletions

View File

@@ -51,7 +51,7 @@ jobs:
} }
- { - {
qt_version: "6.10.3", qt_version: "6.10.3",
qt_creator_version: "19.0.2" qt_creator_version: "19.0.1"
} }
steps: steps:

View File

@@ -1,7 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
list(APPEND CMAKE_PREFIX_PATH "/Users/palm1r/Qt/Qt Creator.sdk/lib/cmake/QtCreator")
project(QodeAssist) project(QodeAssist)
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
@@ -16,9 +14,7 @@ find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
find_package(GTest) find_package(GTest)
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
en cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk
)
# IDE_VERSION is defined by QtCreator package # IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION}) string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
@@ -38,9 +34,10 @@ add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH} -DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
) )
add_subdirectory(sources) add_subdirectory(sources/external/llmqore)
add_subdirectory(logger) add_subdirectory(pluginllmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger)
add_subdirectory(UIControls) add_subdirectory(UIControls)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context) add_subdirectory(context)
@@ -48,11 +45,6 @@ if(GTest_FOUND)
add_subdirectory(test) add_subdirectory(test)
endif() endif()
option(QODEASSIST_BUILD_BENCH "Build the standalone agent bench CLI" ON)
if(QODEASSIST_BUILD_BENCH)
add_subdirectory(bench)
endif()
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
QtCreator::Core QtCreator::Core
@@ -71,9 +63,7 @@ add_qtc_plugin(QodeAssist
QtCreator::Utils QtCreator::Utils
QtCreator::CPlusPlus QtCreator::CPlusPlus
LLMQore LLMQore
ProvidersConfig PluginLLMCore
Agents
Skills
QodeAssistChatViewplugin QodeAssistChatViewplugin
SOURCES SOURCES
.github/workflows/build_cmake.yml .github/workflows/build_cmake.yml
@@ -84,6 +74,49 @@ add_qtc_plugin(QodeAssist
QodeAssisttr.h QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
RefactorContextHelper.hpp RefactorContextHelper.hpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp
templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp
templates/Llama3.hpp
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
templates/Qwen3CoderFIM.hpp
templates/OpenAIResponses.hpp
providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
providers/OpenAIResponses/ModelRequest.hpp
providers/OpenAIResponses/ResponseObject.hpp
providers/OpenAIResponses/GetResponseRequest.hpp
providers/OpenAIResponses/DeleteResponseRequest.hpp
providers/OpenAIResponses/CancelResponseRequest.hpp
providers/OpenAIResponses/ListInputItemsRequest.hpp
providers/OpenAIResponses/InputTokensRequest.hpp
providers/OpenAIResponses/ItemTypesReference.hpp
providers/OpenAIResponsesRequestBuilder.hpp
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
@@ -92,9 +125,7 @@ add_qtc_plugin(QodeAssist
QodeAssistClient.hpp QodeAssistClient.cpp QodeAssistClient.hpp QodeAssistClient.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.cpp chat/ChatOutputPane.h chat/ChatOutputPane.cpp
chat/NavigationPanel.hpp chat/NavigationPanel.cpp chat/NavigationPanel.hpp chat/NavigationPanel.cpp
chat/ChatDocument.hpp chat/ChatDocument.cpp ConfigurationManager.hpp ConfigurationManager.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
CodeHandler.hpp CodeHandler.cpp CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
@@ -126,16 +157,12 @@ add_qtc_plugin(QodeAssist
tools/ReadFileTool.hpp tools/ReadFileTool.cpp tools/ReadFileTool.hpp tools/ReadFileTool.cpp
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
tools/TodoTool.hpp tools/TodoTool.cpp tools/TodoTool.hpp tools/TodoTool.cpp
tools/ReadOriginalHistoryTool.hpp tools/ReadOriginalHistoryTool.cpp
tools/SkillTool.hpp tools/SkillTool.cpp
mcp/McpServerManager.hpp mcp/McpServerManager.cpp mcp/McpServerManager.hpp mcp/McpServerManager.cpp
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
) )
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines Session)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable find_program(QtCreatorExecutable
NAMES NAMES
@@ -157,5 +184,5 @@ endif()
qt_add_translations(TARGETS QodeAssist qt_add_translations(TARGETS QodeAssist
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
RESOURCE_PREFIX "/translations" RESOURCE_PREFIX "/translations"
LUPDATE_OPTIONS -no-obsolete -locations none LUPDATE_OPTIONS -no-obsolete
) )

View File

@@ -23,11 +23,9 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/FileMentionPopup.qml qml/controls/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml qml/controls/FileEditsActionBar.qml
qml/controls/ContextViewer.qml qml/controls/ContextViewer.qml
qml/controls/SkillCommandPopup.qml
qml/controls/Toast.qml qml/controls/Toast.qml
qml/controls/TopBar.qml qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml qml/controls/SplitDropZone.qml
qml/controls/MessageNavigator.qml
RESOURCES RESOURCES
icons/attach-file-light.svg icons/attach-file-light.svg
@@ -45,12 +43,9 @@ qt_add_qml_module(QodeAssistChatView
icons/window-unlock.svg icons/window-unlock.svg
icons/chat-icon.svg icons/chat-icon.svg
icons/chat-pause-icon.svg icons/chat-pause-icon.svg
icons/warning-icon.svg
icons/new-chat-icon.svg
icons/rules-icon.svg icons/rules-icon.svg
icons/context-icon.svg icons/context-icon.svg
icons/open-in-editor.svg icons/open-in-editor.svg
icons/open-in-window.svg
icons/apply-changes-button.svg icons/apply-changes-button.svg
icons/undo-changes-button.svg icons/undo-changes-button.svg
icons/reject-changes-button.svg icons/reject-changes-button.svg
@@ -60,7 +55,6 @@ qt_add_qml_module(QodeAssistChatView
icons/tools-icon-off.svg icons/tools-icon-off.svg
icons/settings-icon.svg icons/settings-icon.svg
icons/compress-icon.svg icons/compress-icon.svg
icons/open-in-code.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
@@ -75,12 +69,7 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp ChatCompressor.hpp ChatCompressor.cpp
ChatAgentController.hpp ChatAgentController.cpp
FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp
FileMentionItem.hpp FileMentionItem.cpp FileMentionItem.hpp FileMentionItem.cpp
SessionFileRegistry.hpp SessionFileRegistry.cpp
) )
target_link_libraries(QodeAssistChatView target_link_libraries(QodeAssistChatView
@@ -91,16 +80,14 @@ target_link_libraries(QodeAssistChatView
Qt::Network Qt::Network
QtCreator::Core QtCreator::Core
QtCreator::Utils QtCreator::Utils
PluginLLMCore
QodeAssistSettings QodeAssistSettings
Context Context
QodeAssistUIControlsplugin QodeAssistUIControlsplugin
QodeAssistLogger QodeAssistLogger
LLMQore LLMQore
Skills
Agents
Session
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
) )

View File

@@ -1,105 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatAgentController.hpp"
#include <QSettings>
#include <coreplugin/icore.h>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <sources/settings/PipelinesConfig.hpp>
namespace QodeAssist::Chat {
namespace {
const char kChatAgentKey[] = "QodeAssist.chatActiveAgent";
}
ChatAgentController::ChatAgentController(QObject *parent)
: QObject(parent)
{
if (auto *settings = Core::ICore::settings())
m_currentAgent = settings->value(kChatAgentKey).toString();
}
void ChatAgentController::setAgentFactory(AgentFactory *factory)
{
m_agentFactory = factory;
reload();
}
QStringList ChatAgentController::availableAgents() const
{
return m_availableAgents;
}
QString ChatAgentController::currentAgent() const
{
return m_currentAgent;
}
void ChatAgentController::setCurrentAgent(const QString &name)
{
if (name == m_currentAgent || !m_availableAgents.contains(name))
return;
m_currentAgent = name;
if (auto *settings = Core::ICore::settings())
settings->setValue(kChatAgentKey, m_currentAgent);
emit currentAgentChanged();
}
void ChatAgentController::reload()
{
const QStringList all = m_agentFactory ? m_agentFactory->configNames() : QStringList{};
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatAssistant;
if (roster.isEmpty()) {
m_availableAgents = all;
} else {
QStringList filtered;
for (const QString &name : roster) {
if (all.contains(name))
filtered.append(name);
}
m_availableAgents = filtered.isEmpty() ? all : filtered;
}
emit availableAgentsChanged();
ensureValidCurrent();
}
void ChatAgentController::ensureValidCurrent()
{
if (m_availableAgents.contains(m_currentAgent))
return;
const QString next = m_availableAgents.isEmpty() ? QString() : m_availableAgents.first();
if (next == m_currentAgent)
return;
m_currentAgent = next;
if (auto *settings = Core::ICore::settings())
settings->setValue(kChatAgentKey, m_currentAgent);
emit currentAgentChanged();
}
bool ChatAgentController::currentSupportsThinking() const
{
if (!m_agentFactory || m_currentAgent.isEmpty())
return false;
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
return config && config->enableThinking;
}
bool ChatAgentController::currentSupportsTools() const
{
if (!m_agentFactory || m_currentAgent.isEmpty())
return false;
const AgentConfig *config = m_agentFactory->configByName(m_currentAgent);
return config && config->enableTools;
}
} // namespace QodeAssist::Chat

View File

@@ -1,47 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QPointer>
#include <QString>
#include <QStringList>
namespace QodeAssist {
class AgentFactory;
}
namespace QodeAssist::Chat {
class ChatAgentController : public QObject
{
Q_OBJECT
public:
explicit ChatAgentController(QObject *parent = nullptr);
void setAgentFactory(AgentFactory *factory);
QStringList availableAgents() const;
QString currentAgent() const;
void setCurrentAgent(const QString &name);
bool currentSupportsThinking() const;
bool currentSupportsTools() const;
void reload();
signals:
void availableAgentsChanged();
void currentAgentChanged();
private:
void ensureValidCurrent();
QPointer<AgentFactory> m_agentFactory;
QStringList m_availableAgents;
QString m_currentAgent;
};
} // namespace QodeAssist::Chat

View File

@@ -1,23 +1,15 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp> #include "ChatModel.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include <QDateTime> #include <QDateTime>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -32,18 +24,7 @@ ChatCompressor::ChatCompressor(QObject *parent)
: QObject(parent) : QObject(parent)
{} {}
void ChatCompressor::setSessionManager(SessionManager *sessionManager) void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
{
m_sessionManager = sessionManager;
}
void ChatCompressor::setActiveAgent(const QString &agentName)
{
m_activeAgent = agentName;
}
void ChatCompressor::startCompression(
const QString &chatFilePath, ConversationHistory *sourceHistory)
{ {
if (m_isCompressing) { if (m_isCompressing) {
emit compressionFailed(tr("Compression already in progress")); emit compressionFailed(tr("Compression already in progress"));
@@ -55,78 +36,47 @@ void ChatCompressor::startCompression(
return; return;
} }
if (!sourceHistory || sourceHistory->isEmpty()) { if (!chatModel || chatModel->rowCount() == 0) {
emit compressionFailed(tr("Chat is empty, nothing to compress")); emit compressionFailed(tr("Chat is empty, nothing to compress"));
return; return;
} }
if (!m_sessionManager) { auto providerName = Settings::generalSettings().caProvider();
emit compressionFailed(tr("Chat session manager is not available")); m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
return; return;
} }
QString sessionError; auto templateName = Settings::generalSettings().caTemplate();
Session *session = m_sessionManager->acquire(m_activeAgent, &sessionError); auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
if (!session) { templateName);
emit compressionFailed(
sessionError.isEmpty() ? tr("No chat agent selected") : sessionError);
return;
}
auto *client = session->client(); if (!promptTemplate) {
if (!client) { emit compressionFailed(tr("No template available"));
m_sessionManager->removeSession(session);
emit compressionFailed(tr("Chat agent has no live client"));
return; return;
} }
m_isCompressing = true; m_isCompressing = true;
m_chatModel = chatModel;
m_originalChatPath = chatFilePath; m_originalChatPath = chatFilePath;
m_session = session; m_accumulatedSummary.clear();
emit compressionStarted(); emit compressionStarted();
session->systemPrompt()->setLayer( connectProviderSignals();
QStringLiteral("compression"),
QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of "
"discussion."));
auto *history = session->history(); QJsonObject payload{
for (const auto &msg : sourceHistory->messages()) { {"model", Settings::generalSettings().caModel()}, {"stream", true}};
if (msg.role() != Message::Role::User && msg.role() != Message::Role::Assistant)
continue;
const QString text = msg.text();
if (text.trimmed().isEmpty())
continue;
Message apiMessage(msg.role()); buildRequestPayload(payload, promptTemplate);
apiMessage.appendBlock(std::make_unique<LLMQore::TextContent>(text));
history->append(std::move(apiMessage));
}
connect( const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
session, &Session::finished, this, const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
[this](const LLMQore::RequestID &id, const QString &) { onCompressionFinished(id); }); : promptTemplate->endpoint();
connect( m_currentRequestId = m_provider->sendRequest(
session, &Session::failed, this, QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onCompressionFailed(id, error.message);
});
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<LLMQore::TextContent>(buildCompressionPrompt()));
m_currentRequestId = session->send(std::move(blocks), /*toolsOverride=*/false);
if (m_currentRequestId.isEmpty()) {
handleCompressionError(tr("Failed to start compression request: %1")
.arg(session->lastError().message));
return;
}
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId)); LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
} }
@@ -141,38 +91,44 @@ void ChatCompressor::cancelCompression()
return; return;
LOG_MESSAGE("Cancelling compression request"); LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState(); cleanupState();
emit compressionFailed(tr("Compression cancelled")); emit compressionFailed(tr("Compression cancelled"));
} }
void ChatCompressor::onCompressionFinished(const QString &requestId) void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
{ {
if (!m_isCompressing || requestId != m_currentRequestId) if (!m_isCompressing || requestId != m_currentRequestId)
return; return;
QString summary; m_accumulatedSummary += partialText;
if (m_session) { }
if (auto *history = m_session->history(); history && !history->isEmpty())
summary = history->messages().back().text();
}
LOG_MESSAGE(QString("Received summary, length: %1 characters").arg(summary.length())); void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
{
Q_UNUSED(fullText)
const QString compressedPath = createCompressedChatPath(m_originalChatPath); if (!m_isCompressing || requestId != m_currentRequestId)
const QString sourcePath = m_originalChatPath; return;
cleanupState(); LOG_MESSAGE(
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
if (!createCompressedChatFile(sourcePath, compressedPath, summary)) { QString compressedPath = createCompressedChatPath(m_originalChatPath);
emit compressionFailed(tr("Failed to save compressed chat")); if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
handleCompressionError(tr("Failed to save compressed chat"));
return; return;
} }
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath)); LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
emit compressionCompleted(compressedPath); emit compressionCompleted(compressedPath);
} }
void ChatCompressor::onCompressionFailed(const QString &requestId, const QString &error) void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
{ {
if (!m_isCompressing || requestId != m_currentRequestId) if (!m_isCompressing || requestId != m_currentRequestId)
return; return;
@@ -209,6 +165,39 @@ QString ChatCompressor::buildCompressionPrompt() const
"Create the summary now:"); "Create the summary now:");
} }
void ChatCompressor::buildRequestPayload(
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
{
PluginLLMCore::ContextData context;
context.systemPrompt = QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of discussion.");
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool
|| msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
PluginLLMCore::Message apiMessage;
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
apiMessage.content = msg.content;
messages.append(apiMessage);
}
PluginLLMCore::Message compressionRequest;
compressionRequest.role = "user";
compressionRequest.content = buildCompressionPrompt();
messages.append(compressionRequest);
context.history = messages;
m_provider->prepareRequest(
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
}
bool ChatCompressor::createCompressedChatFile( bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary) const QString &sourcePath, const QString &destPath, const QString &summary)
{ {
@@ -232,15 +221,13 @@ bool ChatCompressor::createCompressedChatFile(
QJsonObject summaryMessage; QJsonObject summaryMessage;
summaryMessage["role"] = "assistant"; summaryMessage["role"] = "assistant";
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces); summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
QJsonObject textBlock; summaryMessage["isRedacted"] = false;
textBlock["type"] = "text"; summaryMessage["attachments"] = QJsonArray();
textBlock["text"] = QString("# Chat Summary\n\n%1").arg(summary); summaryMessage["images"] = QJsonArray();
summaryMessage["blocks"] = QJsonArray{textBlock};
root["messages"] = QJsonArray{summaryMessage}; root["messages"] = QJsonArray{summaryMessage};
root["compressedFrom"] = sourcePath;
root["compressedAt"] = QDateTime::currentDateTime().toString(Qt::ISODate);
if (QFile::exists(destPath)) if (QFile::exists(destPath))
QFile::remove(destPath); QFile::remove(destPath);
@@ -255,17 +242,49 @@ bool ChatCompressor::createCompressedChatFile(
return true; return true;
} }
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));
}
void ChatCompressor::disconnectAllSignals()
{
for (const auto &connection : std::as_const(m_connections))
disconnect(connection);
m_connections.clear();
}
void ChatCompressor::cleanupState() void ChatCompressor::cleanupState()
{ {
Session *session = m_session; disconnectAllSignals();
m_isCompressing = false; m_isCompressing = false;
m_currentRequestId.clear(); m_currentRequestId.clear();
m_originalChatPath.clear(); m_originalChatPath.clear();
m_session = nullptr; m_accumulatedSummary.clear();
m_chatModel = nullptr;
if (session && m_sessionManager) m_provider = nullptr;
m_sessionManager->release(session);
} }
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,22 +1,22 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QJsonObject>
#include <QList> #include <QList>
#include <QObject> #include <QObject>
#include <QPointer>
#include <QString> #include <QString>
namespace QodeAssist { namespace QodeAssist::PluginLLMCore {
class SessionManager; class Provider;
class Session; class PromptTemplate;
class ConversationHistory; } // namespace QodeAssist::PluginLLMCore
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject class ChatCompressor : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -24,10 +24,7 @@ class ChatCompressor : public QObject
public: public:
explicit ChatCompressor(QObject *parent = nullptr); explicit ChatCompressor(QObject *parent = nullptr);
void setSessionManager(SessionManager *sessionManager); void startCompression(const QString &chatFilePath, ChatModel *chatModel);
void setActiveAgent(const QString &agentName);
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
bool isCompressing() const; bool isCompressing() const;
void cancelCompression(); void cancelCompression();
@@ -37,23 +34,30 @@ signals:
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error); void compressionFailed(const QString &error);
private: private slots:
void onCompressionFinished(const QString &requestId); void onPartialResponseReceived(const QString &requestId, const QString &partialText);
void onCompressionFailed(const QString &requestId, const QString &error); void onFullResponseReceived(const QString &requestId, const QString &fullText);
void onRequestFailed(const QString &requestId, const QString &error);
private:
QString createCompressedChatPath(const QString &originalPath) const; QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const; QString buildCompressionPrompt() const;
bool createCompressedChatFile( bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary); const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState(); void cleanupState();
void handleCompressionError(const QString &error); void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false; bool m_isCompressing = false;
QString m_currentRequestId; QString m_currentRequestId;
QString m_originalChatPath; QString m_originalChatPath;
QPointer<SessionManager> m_sessionManager; QString m_accumulatedSummary;
QString m_activeAgent; PluginLLMCore::Provider *m_provider = nullptr;
QPointer<Session> m_session; ChatModel *m_chatModel = nullptr;
QList<QMetaObject::Connection> m_connections;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatFileManager.hpp" #include "ChatFileManager.hpp"
#include "Logger.hpp" #include "Logger.hpp"

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,240 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatHistoryStore.hpp"
#include <QDateTime>
#include <QDesktopServices>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QRegularExpression>
#include <QUrl>
#include <coreplugin/icore.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "Logger.hpp"
#include "ProjectSettings.hpp"
namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
: QObject(parent)
, m_history(history)
{}
QString ChatHistoryStore::historyDir() const
{
QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString();
} else {
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
path = baseDir.filePath("qodeassist/chat_history");
}
QDir dir(path);
if (!dir.exists() && !dir.mkpath(".")) {
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
return QString();
}
return path;
}
QString ChatHistoryStore::suggestedFileName() const
{
QString shortMessage;
if (m_history) {
for (const auto &message : m_history->messages()) {
if (message.role() != Message::Role::User)
continue;
const QString text = message.text();
if (!text.trimmed().isEmpty()) {
shortMessage = text.split('\n').first().simplified().left(30);
} else {
for (const auto &block : message.blocks()) {
if (dynamic_cast<StoredImageContent *>(block.get())) {
shortMessage = "image_chat";
break;
}
}
}
break;
}
}
return generateChatFileName(shortMessage, historyDir());
}
QString ChatHistoryStore::autosaveFilePath(const QString &recentFilePath) const
{
if (!recentFilePath.isEmpty()) {
return recentFilePath;
}
QString dir = historyDir();
if (dir.isEmpty()) {
return QString();
}
return QDir(dir).filePath(suggestedFileName() + ".json");
}
QString ChatHistoryStore::autosaveFilePath(
const QString &recentFilePath, const QString &firstMessage, bool hasImageAttachments) const
{
if (!recentFilePath.isEmpty()) {
return recentFilePath;
}
QString dir = historyDir();
if (dir.isEmpty()) {
return QString();
}
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
if (shortMessage.isEmpty() && hasImageAttachments) {
shortMessage = "image_chat";
}
QString fileName = generateChatFileName(shortMessage, dir);
return QDir(dir).filePath(fileName + ".json");
}
SerializationResult ChatHistoryStore::save(const QString &filePath) const
{
return ChatSerializer::saveToFile(m_history, filePath);
}
SerializationResult ChatHistoryStore::load(const QString &filePath) const
{
return ChatSerializer::loadFromFile(m_history, filePath);
}
void ChatHistoryStore::showSaveDialog()
{
QString initialDir = historyDir();
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
dialog->setAcceptMode(QFileDialog::AcceptSave);
dialog->setFileMode(QFileDialog::AnyFile);
dialog->setNameFilter(tr("JSON files (*.json)"));
dialog->setDefaultSuffix("json");
if (!initialDir.isEmpty()) {
dialog->setDirectory(initialDir);
dialog->selectFile(suggestedFileName() + ".json");
}
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
if (result == QFileDialog::Accepted) {
QStringList files = dialog->selectedFiles();
if (!files.isEmpty()) {
emit saveRequested(files.first());
}
}
dialog->deleteLater();
});
dialog->open();
}
void ChatHistoryStore::showLoadDialog()
{
QString initialDir = historyDir();
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
dialog->setAcceptMode(QFileDialog::AcceptOpen);
dialog->setFileMode(QFileDialog::ExistingFile);
dialog->setNameFilter(tr("JSON files (*.json)"));
if (!initialDir.isEmpty()) {
dialog->setDirectory(initialDir);
}
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
if (result == QFileDialog::Accepted) {
QStringList files = dialog->selectedFiles();
if (!files.isEmpty()) {
emit loadRequested(files.first());
}
}
dialog->deleteLater();
});
dialog->open();
}
void ChatHistoryStore::openHistoryFolder() const
{
QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString();
} else {
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
path = baseDir.filePath("qodeassist/chat_history");
}
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
QString ChatHistoryStore::generateChatFileName(const QString &shortMessage, const QString &dir) const
{
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
static const QRegularExpression underSymbols = QRegularExpression("_+");
QStringList parts;
QString sanitizedMessage = shortMessage;
sanitizedMessage.replace(saitizeSymbols, "_");
sanitizedMessage.replace(underSymbols, "_");
sanitizedMessage = sanitizedMessage.trimmed();
if (!sanitizedMessage.isEmpty()) {
if (sanitizedMessage.startsWith('_')) {
sanitizedMessage.remove(0, 1);
}
if (sanitizedMessage.endsWith('_')) {
sanitizedMessage.chop(1);
}
QString fullPath = QDir(dir).filePath(sanitizedMessage);
QFileInfo fileInfo(fullPath);
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
parts << sanitizedMessage;
}
}
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
QString fileName = parts.join("_");
QString fullPath = QDir(dir).filePath(fileName);
QFileInfo finalCheck(fullPath);
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
}
return fileName;
}
} // namespace QodeAssist::Chat

View File

@@ -1,50 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QString>
#include "ChatSerializer.hpp"
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat {
class ChatHistoryStore : public QObject
{
Q_OBJECT
public:
explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
QString historyDir() const;
QString suggestedFileName() const;
QString autosaveFilePath(const QString &recentFilePath) const;
QString autosaveFilePath(
const QString &recentFilePath,
const QString &firstMessage,
bool hasImageAttachments) const;
SerializationResult save(const QString &filePath) const;
SerializationResult load(const QString &filePath) const;
void showSaveDialog();
void showLoadDialog();
void openHistoryFolder() const;
signals:
void saveRequested(const QString &filePath);
void loadRequested(const QString &filePath);
private:
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
ConversationHistory *m_history;
};
} // namespace QodeAssist::Chat

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -8,133 +7,107 @@
#include "MessagePart.hpp" #include "MessagePart.hpp"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QHash>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject>
#include <QPointer>
#include <QVector>
#include <QtQmlIntegration> #include <QtQmlIntegration>
namespace QodeAssist { #include "context/ContentFile.hpp"
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel : public QAbstractListModel class ChatModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking }; enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted, Images };
RoleType = Qt::UserRole,
Content,
Attachments,
IsRedacted,
Images,
PromptTokens,
CompletionTokens,
CachedPromptTokens,
ReasoningTokens,
TotalTokens
};
Q_ENUM(Roles) Q_ENUM(Roles)
explicit ChatModel(QObject *parent = nullptr); struct ImageAttachment
{
QString fileName; // Original filename
QString storedPath; // Path to stored image file (relative to chat folder)
QString mediaType; // MIME type
};
void setHistory(ConversationHistory *history); struct Message
{
ChatRole role;
QString content;
QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments;
QList<ImageAttachment> images;
};
explicit ChatModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {},
bool isRedacted = false,
const QString &signature = QString());
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
int tokensThreshold() const;
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index); Q_INVOKABLE void resetModelTo(int index);
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
void setMessageUsage( void addToolExecutionStatus(
const QString &messageId, const QString &requestId, const QString &toolId, const QString &toolName);
int promptTokens, void updateToolResult(
int completionTokens, const QString &requestId,
int cachedPromptTokens, const QString &toolId,
int reasoningTokens); const QString &toolName,
const QString &result);
void addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature);
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent);
int sessionPromptTokens() const; void setLoadingFromHistory(bool loading);
int sessionCompletionTokens() const; bool isLoadingFromHistory() const;
int sessionCachedPromptTokens() const;
int sessionTotalTokens() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
signals: signals:
void tokensThresholdChanged();
void modelReseted(); void modelReseted();
void sessionUsageChanged();
private slots: private slots:
void onHistoryMessageAdded(int index); void onFileEditApplied(const QString &editId);
void onHistoryMessageUpdated(int index); void onFileEditRejected(const QString &editId);
void onHistoryCleared(); void onFileEditArchived(const QString &editId);
void onHistoryReset();
void onFileEditStatusChanged(const QString &editId);
private: private:
struct AttachmentRef void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
{
QString fileName;
QString storedPath;
};
struct ImageRef
{
QString fileName;
QString storedPath;
QString mediaType;
};
struct Row
{
ChatRole kind = ChatRole::Assistant;
int messageIndex = -1;
QString messageId;
QString content;
bool isRedacted = false;
QString editId;
QVector<AttachmentRef> attachments;
QVector<ImageRef> images;
};
struct Usage
{
int prompt = 0;
int completion = 0;
int cached = 0;
int reasoning = 0;
};
void rebuildAll(); QVector<Message> m_messages;
void reprojectTail(int startMessageIndex); bool m_loadingFromHistory = false;
int startMessageIndexFor(int messageIndex) const;
int firstRowForMessage(int messageIndex) const;
QHash<QString, QString> buildToolResultMap() const;
void appendRowsForMessage(
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
QVariantList buildImageList(const QVector<ImageRef> &images) const;
QPointer<ConversationHistory> m_history;
QVector<Row> m_rows;
QHash<QString, Usage> m_usageByMessageId;
QString m_chatFilePath; QString m_chatFilePath;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart) Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,26 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QPointer>
#include <QQuickItem> #include <QQuickItem>
#include <QVariantList> #include <QVariantList>
#include "ChatFileManager.hpp" #include "ChatFileManager.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist {
class AgentFactory;
class SessionManager;
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor; class ChatCompressor;
class ChatAgentController;
class FileEditController;
class InputTokenCounter;
class ChatHistoryStore;
class SessionFileRegistry;
class ChatRootView : public QQuickItem class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL) Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL) Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL) Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
@@ -49,30 +34,32 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL) Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL) Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL) Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking NOTIFY useThinkingChanged FINAL) Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL) Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL) Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL) Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
Q_PROPERTY(QStringList availableRoles READ availableRoles NOTIFY availableRolesChanged FINAL) Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
Q_PROPERTY(QString currentRole READ currentRole WRITE setCurrentRole NOTIFY currentRoleChanged 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) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
ChatRootView(QQuickItem *parent = nullptr); ChatRootView(QQuickItem *parent = nullptr);
~ChatRootView() override;
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const;
void saveHistory(const QString &filePath); void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath); void loadHistory(const QString &filePath);
@@ -97,19 +84,13 @@ public:
Q_INVOKABLE void showAddImageDialog(); Q_INVOKABLE void showAddImageDialog();
Q_INVOKABLE bool isImageFile(const QString &filePath) const; Q_INVOKABLE bool isImageFile(const QString &filePath) const;
Q_INVOKABLE void calculateMessageTokensCount(const QString &message); Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE bool isSendShortcut(int key, int modifiers) const;
QString sendShortcutText() const;
Q_INVOKABLE void setIsSyncOpenFiles(bool state); Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder(); Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings(); Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath); Q_INVOKABLE void openFileInEditor(const QString &filePath);
Q_INVOKABLE void relocateToSplit();
Q_INVOKABLE void relocateToWindow();
void consumePendingChatFile();
Q_INVOKABLE void updateInputTokensCount(); Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const; int inputTokensCount() const;
@@ -136,10 +117,15 @@ public:
QString lastErrorMessage() const; QString lastErrorMessage() const;
Q_INVOKABLE QVariantList searchSkills(const QString &query) const; QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
bool useTools() const; bool useTools() const;
void setUseTools(bool enabled);
bool useThinking() const; bool useThinking() const;
void setUseThinking(bool enabled);
Q_INVOKABLE void applyFileEdit(const QString &editId); Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId); Q_INVOKABLE void rejectFileEdit(const QString &editId);
@@ -150,18 +136,22 @@ public:
Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats(); Q_INVOKABLE void updateCurrentMessageEditsStats();
Q_INVOKABLE void loadAvailableConfigurations();
Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
Q_INVOKABLE void compressCurrentChat(); Q_INVOKABLE void compressCurrentChat();
Q_INVOKABLE void cancelCompression(); Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableChatAgents(); Q_INVOKABLE void loadAvailableAgentRoles();
QStringList availableChatAgents() const; Q_INVOKABLE void applyAgentRole(const QString &roleId);
QString currentChatAgent() const; Q_INVOKABLE void openAgentRolesSettings();
void setCurrentChatAgent(const QString &name); QStringList availableAgentRoles() const;
QString currentAgentRole() const;
Q_INVOKABLE void loadAvailableRoles(); QString baseSystemPrompt() const;
QStringList availableRoles() const; QString currentAgentRoleDescription() const;
QString currentRole() const; QString currentAgentRoleSystemPrompt() const;
void setCurrentRole(const QString &roleId);
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
@@ -174,13 +164,6 @@ public:
bool isCompressing() const; bool isCompressing() const;
bool isInEditor() const;
void setInEditor(bool value);
QString chatTitle() const;
Q_INVOKABLE void requestNewChat();
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message);
void copyToClipboard(const QString &text); void copyToClipboard(const QString &text);
@@ -191,6 +174,7 @@ public slots:
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged(); void attachmentFilesChanged();
void linkedFilesChanged(); void linkedFilesChanged();
void inputTokensCountChanged(); void inputTokensCountChanged();
@@ -206,87 +190,64 @@ signals:
void lastErrorMessageChanged(); void lastErrorMessageChanged();
void lastInfoMessageChanged(); void lastInfoMessageChanged();
void sendShortcutTextChanged(); void activeRulesChanged();
void activeRulesCountChanged();
void useToolsChanged(); void useToolsChanged();
void useThinkingChanged(); void useThinkingChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged(); void isThinkingSupportChanged();
void availableConfigurationsChanged();
void currentConfigurationChanged();
void availableChatAgentsChanged(); void availableAgentRolesChanged();
void currentChatAgentChanged(); void currentAgentRoleChanged();
void availableRolesChanged(); void baseSystemPromptChanged();
void currentRoleChanged();
void isCompressingChanged(); void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error); void compressionFailed(const QString &error);
void isInEditorChanged();
void chatTitleChanged();
void openFilesChanged(); void openFilesChanged();
void closeHostRequested();
private: private:
QString computeChatTitle() const; void updateFileEditStatus(const QString &editId, const QString &status);
void triggerOpenChatCommand(Utils::Id commandId); QString getChatsHistoryDir() const;
void handOffSession(); QString getSuggestedFileName() const;
bool deferSendForAutoCompress( QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles);
void dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles);
bool hasImageAttachments(const QStringList &attachments) const; bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const;
AgentFactory *agentFactory() const;
SessionManager *sessionManager() const;
QodeAssist::ConversationHistory *m_history;
ChatModel *m_chatModel; ChatModel *m_chatModel;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager; ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath; QString m_recentFilePath;
QStringList m_attachmentFiles; QStringList m_attachmentFiles;
QStringList m_linkedFiles; QStringList m_linkedFiles;
int m_messageTokensCount{0};
struct PendingSend { int m_inputTokensCount{0};
QString message;
QStringList attachments;
QStringList linkedFiles;
bool active = false;
};
PendingSend m_pendingSend;
bool m_isSyncOpenFiles; bool m_isSyncOpenFiles;
bool m_isInEditor = false;
mutable QString m_cachedChatTitle;
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0};
int m_currentMessageAppliedEdits{0};
int m_currentMessagePendingEdits{0};
int m_currentMessageRejectedEdits{0};
QString m_lastInfoMessage; QString m_lastInfoMessage;
QString m_currentRole = QStringLiteral("developer"); QStringList m_availableConfigurations;
QStringList m_availableRoles; QString m_currentConfiguration;
QStringList m_availableAgentRoles;
QString m_currentAgentRole;
ChatCompressor *m_chatCompressor; ChatCompressor *m_chatCompressor;
ChatAgentController *m_agentController;
FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore;
mutable QPointer<SessionFileRegistry> m_sessionFileRegistry;
mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false;
mutable QPointer<AgentFactory> m_agentFactory;
mutable QPointer<SessionManager> m_sessionManager;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,12 +1,10 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include <memory> #include <QBuffer>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
@@ -14,57 +12,12 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <LLMQore/ContentBlocks.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <MessageSerializer.hpp>
#include <PluginBlocks.hpp>
#include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
namespace { const QString ChatSerializer::VERSION = "0.2";
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:"); SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
// Legacy (<= 0.2) per-row ChatRole values, kept only for importing old chat files.
enum class LegacyRole { System = 0, User = 1, Assistant = 2, Tool = 3, FileEdit = 4, Thinking = 5 };
void registerEditFromResult(const QString &result)
{ {
const int pos = result.indexOf(kFileEditMarker);
if (pos < 0)
return;
const QString jsonStr = result.mid(pos + kFileEditMarker.length());
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (!doc.isObject())
return;
const QJsonObject obj = doc.object();
const QString editId = obj.value("edit_id").toString();
const QString filePath = obj.value("file").toString();
if (editId.isEmpty() || filePath.isEmpty())
return;
Context::ChangesManager::instance().addFileEdit(
editId,
filePath,
obj.value("old_content").toString(),
obj.value("new_content").toString(),
/*autoApply=*/false,
/*isFromHistory=*/true);
}
} // namespace
const QString ChatSerializer::VERSION = "0.3";
SerializationResult ChatSerializer::saveToFile(
const ConversationHistory *history, const QString &filePath)
{
if (!history)
return {false, "No conversation history"};
if (!ensureDirectoryExists(filePath)) { if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"}; return {false, "Failed to create directory structure"};
} }
@@ -74,7 +27,9 @@ SerializationResult ChatSerializer::saveToFile(
return {false, QString("Failed to open file for writing: %1").arg(filePath)}; return {false, QString("Failed to open file for writing: %1").arg(filePath)};
} }
QJsonDocument doc(serializeChat(history)); QJsonObject root = serializeChat(model, filePath);
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) { if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())}; return {false, QString("Failed to write to file: %1").arg(file.errorString())};
} }
@@ -82,12 +37,8 @@ SerializationResult ChatSerializer::saveToFile(
return {true, QString()}; return {true, QString()};
} }
SerializationResult ChatSerializer::loadFromFile( SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
ConversationHistory *history, const QString &filePath)
{ {
if (!history)
return {false, "No conversation history"};
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)}; return {false, QString("Failed to open file for reading: %1").arg(filePath)};
@@ -99,140 +50,145 @@ SerializationResult ChatSerializer::loadFromFile(
return {false, QString("JSON parse error: %1").arg(error.errorString())}; return {false, QString("JSON parse error: %1").arg(error.errorString())};
} }
const QJsonObject root = doc.object(); QJsonObject root = doc.object();
const QString version = root["version"].toString(); QString version = root["version"].toString();
if (!validateVersion(version)) { if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)}; return {false, QString("Unsupported version: %1").arg(version)};
} }
if (version == VERSION) if (!deserializeChat(model, root, filePath)) {
return loadCurrent(history, root); return {false, "Failed to deserialize chat data"};
return loadLegacy(history, root); }
return {true, QString()};
} }
QJsonObject ChatSerializer::serializeChat(const ConversationHistory *history) QJsonObject ChatSerializer::serializeMessage(
const ChatModel::Message &message, const QString &chatFilePath)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
if (message.isRedacted) {
messageObj["isRedacted"] = true;
}
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) {
QJsonObject attachmentObj;
attachmentObj["fileName"] = attachment.filename;
attachmentObj["storedPath"] = attachment.content;
attachmentsArray.append(attachmentObj);
}
messageObj["attachments"] = attachmentsArray;
}
if (!message.images.isEmpty()) {
QJsonArray imagesArray;
for (const auto &image : message.images) {
QJsonObject imageObj;
imageObj["fileName"] = image.fileName;
imageObj["storedPath"] = image.storedPath;
imageObj["mediaType"] = image.mediaType;
imagesArray.append(imageObj);
}
messageObj["images"] = imagesArray;
}
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(
const QJsonObject &json, const QString &chatFilePath)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray();
for (const auto &attachmentValue : attachmentsArray) {
QJsonObject attachmentObj = attachmentValue.toObject();
Context::ContentFile attachment;
attachment.filename = attachmentObj["fileName"].toString();
attachment.content = attachmentObj["storedPath"].toString();
message.attachments.append(attachment);
}
}
if (json.contains("images")) {
QJsonArray imagesArray = json["images"].toArray();
for (const auto &imageValue : imagesArray) {
QJsonObject imageObj = imageValue.toObject();
ChatModel::ImageAttachment image;
image.fileName = imageObj["fileName"].toString();
image.storedPath = imageObj["storedPath"].toString();
image.mediaType = imageObj["mediaType"].toString();
message.images.append(image);
}
}
return message;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
{ {
QJsonArray messagesArray; QJsonArray messagesArray;
for (const auto &message : history->messages()) for (const auto &message : model->getChatHistory()) {
messagesArray.append(MessageSerializer::toJson(message)); messagesArray.append(serializeMessage(message, chatFilePath));
}
QJsonObject root; QJsonObject root;
root["version"] = VERSION; root["version"] = VERSION;
root["messages"] = messagesArray; root["messages"] = messagesArray;
return root; return root;
} }
SerializationResult ChatSerializer::loadCurrent(ConversationHistory *history, const QJsonObject &root) bool ChatSerializer::deserializeChat(
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
{ {
history->clear(); QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
const QJsonArray messagesArray = root["messages"].toArray(); for (const auto &messageValue : messagesArray) {
for (const auto &value : messagesArray) { messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
bool ok = false;
Message message = MessageSerializer::fromJson(value.toObject(), &ok);
if (ok)
history->append(std::move(message));
} }
registerHistoricalFileEdits(history); model->clear();
return {true, QString()};
}
SerializationResult ChatSerializer::loadLegacy(ConversationHistory *history, const QJsonObject &root) model->setLoadingFromHistory(true);
{
history->clear();
const QJsonArray arr = root["messages"].toArray(); for (const auto &message : messages) {
int i = 0; model->addMessage(
while (i < arr.size()) { message.content,
const QJsonObject mj = arr[i].toObject(); message.role,
const auto role = static_cast<LegacyRole>(mj["role"].toInt()); message.id,
message.attachments,
if (role == LegacyRole::Tool) { message.images,
Message assistant(Message::Role::Assistant); message.isRedacted,
Message toolResults(Message::Role::User); message.signature);
while (i < arr.size() LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
&& static_cast<LegacyRole>(arr[i].toObject()["role"].toInt()) == LegacyRole::Tool) { .arg(message.images.size())
const QJsonObject tj = arr[i].toObject(); .arg(message.isRedacted)
const QString toolName = tj["toolName"].toString(); .arg(message.signature.length()));
const QString id = tj["id"].toString();
if (!toolName.isEmpty()) {
assistant.appendBlock(std::make_unique<LLMQore::ToolUseContent>(
id, toolName, tj["toolArguments"].toObject()));
toolResults.appendBlock(std::make_unique<LLMQore::ToolResultContent>(
id, tj["toolResult"].toString()));
}
++i;
}
if (!assistant.blocks().empty()) {
history->append(std::move(assistant));
history->append(std::move(toolResults));
}
continue;
}
++i;
if (role == LegacyRole::FileEdit)
continue; // derived from the tool result in the new model
if (role == LegacyRole::Thinking) {
const QString content = mj["content"].toString();
const QString signature = mj["signature"].toString();
Message assistant(Message::Role::Assistant);
if (mj["isRedacted"].toBool(false)) {
assistant.appendBlock(
std::make_unique<LLMQore::RedactedThinkingContent>(signature));
} else {
const int sigPos = content.indexOf(QStringLiteral("\n[Signature:"));
const QString thinking = sigPos >= 0 ? content.left(sigPos) : content;
assistant.appendBlock(
std::make_unique<LLMQore::ThinkingContent>(thinking, signature));
}
history->append(std::move(assistant));
continue;
}
if (role == LegacyRole::User) {
Message user(Message::Role::User, mj["id"].toString());
user.appendBlock(std::make_unique<LLMQore::TextContent>(mj["content"].toString()));
for (const auto &a : mj["attachments"].toArray()) {
const QJsonObject ao = a.toObject();
user.appendBlock(std::make_unique<StoredAttachmentContent>(
ao["fileName"].toString(), ao["storedPath"].toString()));
}
for (const auto &im : mj["images"].toArray()) {
const QJsonObject io = im.toObject();
user.appendBlock(std::make_unique<StoredImageContent>(
io["fileName"].toString(),
io["storedPath"].toString(),
io["mediaType"].toString()));
}
history->append(std::move(user));
} else {
const QString content = mj["content"].toString();
if (content.trimmed().isEmpty())
continue;
const Message::Role mapped
= role == LegacyRole::System ? Message::Role::System : Message::Role::Assistant;
Message message(mapped, mj["id"].toString());
message.appendBlock(std::make_unique<LLMQore::TextContent>(content));
history->append(std::move(message));
}
} }
registerHistoricalFileEdits(history); model->setLoadingFromHistory(false);
return {true, QString()};
}
void ChatSerializer::registerHistoricalFileEdits(const ConversationHistory *history) return true;
{
for (const auto &message : history->messages()) {
for (const auto &block : message.blocks()) {
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
registerEditFromResult(tr->result());
}
}
} }
bool ChatSerializer::ensureDirectoryExists(const QString &filePath) bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
@@ -244,7 +200,18 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
bool ChatSerializer::validateVersion(const QString &version) bool ChatSerializer::validateVersion(const QString &version)
{ {
return version == VERSION || version == "0.2" || version == "0.1"; 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::getChatContentFolder(const QString &chatFilePath) QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)

View File

@@ -1,15 +1,13 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
namespace QodeAssist { #include "ChatModel.hpp"
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -22,26 +20,26 @@ struct SerializationResult
class ChatSerializer class ChatSerializer
{ {
public: public:
static SerializationResult saveToFile( static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
const ConversationHistory *history, const QString &filePath); static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
// Public for testing purposes
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
// Content management (images and text files) // Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath); static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage( static bool saveContentToStorage(const QString &chatFilePath,
const QString &chatFilePath, const QString &fileName,
const QString &fileName, const QString &base64Data,
const QString &base64Data, QString &storedPath);
QString &storedPath);
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath); static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
private: private:
static const QString VERSION; static const QString VERSION;
static constexpr int CURRENT_VERSION = 1;
static QJsonObject serializeChat(const ConversationHistory *history);
static SerializationResult loadCurrent(ConversationHistory *history, const QJsonObject &root);
static SerializationResult loadLegacy(ConversationHistory *history, const QJsonObject &root);
static void registerHistoricalFileEdits(const ConversationHistory *history);
static bool ensureDirectoryExists(const QString &filePath); static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version); static bool validateVersion(const QString &version);

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatUtils.h" #include "ChatUtils.h"
@@ -20,34 +19,22 @@ QString ChatUtils::getSafeMarkdownText(const QString &text) const
return text; return text;
} }
bool needsSanitization = false;
for (const QChar &ch : text) {
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
needsSanitization = true;
break;
}
}
if (!needsSanitization) {
return text;
}
QString safeText; QString safeText;
safeText.reserve(text.size() + 16); safeText.reserve(text.size());
bool inFenced = false;
bool inInline = false;
for (int i = 0; i < text.size(); ++i) {
const QChar ch = text[i];
if (!inInline && i + 2 < text.size()
&& text[i] == '`' && text[i + 1] == '`' && text[i + 2] == '`') {
safeText.append(QStringLiteral("```"));
inFenced = !inFenced;
i += 2;
continue;
}
if (!inFenced && ch == '`') {
safeText.append(ch);
inInline = !inInline;
continue;
}
if (!inFenced && !inInline && ch == '<') {
safeText.append(QStringLiteral("&lt;"));
continue;
}
for (QChar ch : text) {
if (ch.isNull()) { if (ch.isNull()) {
safeText.append(' '); safeText.append(' ');
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') { } else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,25 +1,16 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatView.hpp" #include "ChatView.hpp"
#include <QQmlComponent>
#include <QQmlContext> #include <QQmlContext>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQuickItem>
#include <QSettings> #include <QSettings>
#include <QVariantMap> #include <QVariantMap>
#include <coreplugin/actionmanager/actionmanager.h> #include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include "ChatRootView.hpp"
#include "QodeAssistConstants.hpp"
#include "SessionFileRegistry.hpp"
#include "sources/skills/SkillsManager.hpp"
namespace { namespace {
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
@@ -28,65 +19,30 @@ constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::Win
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatView::ChatView( ChatView::ChatView()
QQmlEngine *engine, : m_isPin(false)
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager)
: QQuickView{engine, nullptr}
, m_isPin(false)
{ {
setTitle("QodeAssist Chat"); setTitle("QodeAssist Chat");
/// @note setup quick view content engine()->rootContext()->setContextProperty("_chatview", this);
{ setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
auto context = new QQmlContext{engine, this};
context->setContextProperty("_chatview", this);
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
context->setContextProperty("skillsManager", skillsManager);
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
auto rootItem = component->create(context);
setContent(component->url(), component, rootItem);
}
if (auto rootView = qobject_cast<ChatRootView *>(rootObject())) {
connect(
rootView,
&ChatRootView::closeHostRequested,
this,
&QWindow::close,
Qt::QueuedConnection);
}
setResizeMode(QQuickView::SizeRootObjectToView); setResizeMode(QQuickView::SizeRootObjectToView);
setMinimumSize({400, 300}); setMinimumSize({400, 300});
setFlags(baseFlags); setFlags(baseFlags);
bindCommandShortcut("QodeAssist.CloseChatView", [this] { close(); }); if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE, [this] { m_closeShortcut = new QShortcut(action->keySequence(), this);
QMetaObject::invokeMethod(rootObject(), "sendChatMessage"); connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
});
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_CLEAR_SESSION, [this] { connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
QMetaObject::invokeMethod(rootObject(), "clearChat"); if (m_closeShortcut) {
}); m_closeShortcut->setKey(action->keySequence());
}
});
}
restoreSettings(); restoreSettings();
} }
void ChatView::bindCommandShortcut(Utils::Id commandId,
const std::function<void()> &onActivated)
{
auto command = Core::ActionManager::command(commandId);
if (!command)
return;
auto shortcut = new QShortcut(command->keySequence(), this);
connect(shortcut, &QShortcut::activated, this, onActivated);
connect(command, &Core::Command::keySequenceChanged, shortcut, [command, shortcut]() {
shortcut->setKey(command->keySequence());
});
}
void ChatView::closeEvent(QCloseEvent *event) void ChatView::closeEvent(QCloseEvent *event)
{ {
saveSettings(); saveSettings();

View File

@@ -1,33 +1,19 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <functional>
#include <utils/id.h>
#include <QQuickView> #include <QQuickView>
#include <QShortcut> #include <QShortcut>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class SessionFileRegistry;
class ChatView : public QQuickView class ChatView : public QQuickView
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL) Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
public: public:
ChatView( ChatView();
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager);
bool isPin() const; bool isPin() const;
void setIsPin(bool newIsPin); void setIsPin(bool newIsPin);
@@ -41,9 +27,9 @@ protected:
private: private:
void saveSettings(); void saveSettings();
void restoreSettings(); void restoreSettings();
void bindCommandShortcut(Utils::Id commandId, const std::function<void()> &onActivated);
bool m_isPin; bool m_isPin;
QShortcut *m_closeShortcut;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,68 +1,18 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatWidget.hpp" #include "ChatWidget.hpp"
#include <QApplication>
#include <QMouseEvent>
#include <QQmlContext> #include <QQmlContext>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQuickItem>
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
#include "QodeAssistConstants.hpp"
#include "SessionFileRegistry.hpp"
#include "sources/skills/SkillsManager.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatWidget::ChatWidget( ChatWidget::ChatWidget(QWidget *parent)
QQmlEngine *engine, : QQuickWidget(parent)
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager,
bool registerOwnContext,
QWidget *parent)
: QQuickWidget{engine, parent}
{ {
/// @note setup quick view content setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
{
auto context = new QQmlContext{engine, this};
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
context->setContextProperty("skillsManager", skillsManager);
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
auto rootItem = component->create(context);
setContent(component->url(), component, rootItem);
}
setResizeMode(QQuickWidget::SizeRootObjectToView); setResizeMode(QQuickWidget::SizeRootObjectToView);
setFocusPolicy(Qt::StrongFocus);
setAttribute(Qt::WA_NoMousePropagation, true);
if (registerOwnContext) {
auto ideContext = new Core::IContext{this};
ideContext->setWidget(this);
ideContext->setContext(Core::Context{Constants::QODE_ASSIST_CHAT_CONTEXT});
Core::ICore::addContextObject(ideContext);
}
}
void ChatWidget::focusInEvent(QFocusEvent *event)
{
QQuickWidget::focusInEvent(event);
if (rootObject())
QMetaObject::invokeMethod(rootObject(), "focusInput");
}
void ChatWidget::mousePressEvent(QMouseEvent *event)
{
if (!hasFocus())
setFocus(Qt::MouseFocusReason);
QQuickWidget::mousePressEvent(event);
} }
void ChatWidget::clear() void ChatWidget::clear()
@@ -74,35 +24,4 @@ void ChatWidget::scrollToBottom()
{ {
QMetaObject::invokeMethod(rootObject(), "scrollToBottom"); QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
} }
void ChatWidget::focusInput()
{
setFocus(Qt::OtherFocusReason);
QMetaObject::invokeMethod(rootObject(), "focusInput");
}
bool ChatWidget::isChatFocused() const
{
return hasFocus() || (rootObject() && rootObject()->hasActiveFocus());
}
void ChatWidget::sendMessage()
{
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
}
void ChatWidget::clearSession()
{
QMetaObject::invokeMethod(rootObject(), "clearChat");
}
ChatWidget *ChatWidget::focusedInstance()
{
for (QWidget *widget = QApplication::focusWidget(); widget;
widget = widget->parentWidget()) {
if (auto chatWidget = qobject_cast<ChatWidget *>(widget))
return chatWidget;
}
return nullptr;
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,49 +1,25 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QtQuickWidgets/QtQuickWidgets> #include <QtQuickWidgets/QtQuickWidgets>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class SessionFileRegistry;
class ChatWidget : public QQuickWidget class ChatWidget : public QQuickWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ChatWidget( explicit ChatWidget(QWidget *parent = nullptr);
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager,
bool registerOwnContext = true,
QWidget *parent = nullptr);
~ChatWidget() = default; ~ChatWidget() = default;
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE void scrollToBottom(); Q_INVOKABLE void scrollToBottom();
Q_INVOKABLE void focusInput();
void sendMessage();
void clearSession();
bool isChatFocused() const;
static ChatWidget *focusedInstance();
signals: signals:
void clearPressed(); void clearPressed();
protected:
void focusInEvent(QFocusEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,70 +1,51 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <memory>
#include <vector>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolsManager.hpp> #include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/target.h>
#include <texteditor/textdocument.h>
#include <QFile>
#include <QFileInfo>
#include <QImageReader>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QUuid>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h> #include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h> #include <coreplugin/idocument.h>
#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include <projectexplorer/target.h>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <QDir> #include <LLMQore/ToolsManager.hpp>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QUuid>
#include <ConversationHistory.hpp>
#include <ContextRenderer.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "tools/ReadOriginalHistoryTool.hpp"
#include "tools/TodoTool.hpp" #include "tools/TodoTool.hpp"
#include "tools/ToolsRegistration.hpp"
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProvidersManager.hpp"
#include "SkillsSettings.hpp"
#include "ToolsSettings.hpp" #include "ToolsSettings.hpp"
#include <RulesLoader.hpp>
#include <context/ChangesManager.h> #include <context/ChangesManager.h>
#include <sources/skills/SkillsManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
namespace { ClientInterface::ClientInterface(
struct StoredImage ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
{
QString fileName;
QString storedPath;
QString mediaType;
};
} // namespace
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
{} {}
@@ -73,52 +54,27 @@ ClientInterface::~ClientInterface()
cancelRequest(); cancelRequest();
} }
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
{
m_skillsManager = skillsManager;
}
void ClientInterface::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void ClientInterface::setHistory(ConversationHistory *history)
{
m_history = history;
}
void ClientInterface::setActiveAgent(const QString &agentName)
{
m_activeAgent = agentName;
}
void ClientInterface::setActiveRole(const QString &roleId)
{
m_activeRoleId = roleId;
}
void ClientInterface::sendMessage( void ClientInterface::sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments, const QList<QString> &attachments,
const QList<QString> &linkedFiles) const QList<QString> &linkedFiles,
bool useTools,
bool useThinking)
{ {
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message");
return;
}
cancelRequest(); cancelRequest();
m_accumulatedResponses.clear();
Context::ChangesManager::instance().archiveAllNonArchivedEdits(); Context::ChangesManager::instance().archiveAllNonArchivedEdits();
QList<QString> imageFiles; QList<QString> imageFiles;
QList<QString> textFiles; QList<QString> textFiles;
for (const QString &filePath : attachments) { for (const QString &filePath : attachments) {
if (isImageFile(filePath)) if (isImageFile(filePath)) {
imageFiles.append(filePath); imageFiles.append(filePath);
else } else {
textFiles.append(filePath); textFiles.append(filePath);
}
} }
QList<Context::ContentFile> storedAttachments; QList<Context::ContentFile> storedAttachments;
@@ -140,19 +96,24 @@ void ClientInterface::sendMessage(
.arg(textFiles.size())); .arg(textFiles.size()));
} }
QList<StoredImage> storedImages; QList<ChatModel::ImageAttachment> imageAttachments;
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const QString &imagePath : imageFiles) { for (const QString &imagePath : imageFiles) {
QString base64Data = encodeImageToBase64(imagePath); QString base64Data = encodeImageToBase64(imagePath);
if (base64Data.isEmpty()) if (base64Data.isEmpty()) {
continue; continue;
}
QString storedPath; QString storedPath;
QFileInfo fileInfo(imagePath); QFileInfo fileInfo(imagePath);
if (ChatSerializer::saveContentToStorage( if (ChatSerializer::saveContentToStorage(
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) { m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
storedImages.append( ChatModel::ImageAttachment imageAttachment;
{fileInfo.fileName(), storedPath, getMediaTypeForImage(imagePath)}); imageAttachment.fileName = fileInfo.fileName();
imageAttachment.storedPath = storedPath;
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
imageAttachments.append(imageAttachment);
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath)); LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
} }
} }
@@ -161,287 +122,238 @@ void ClientInterface::sendMessage(
.arg(imageFiles.size())); .arg(imageFiles.size()));
} }
if (!m_sessionManager) { m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
const QString error = QStringLiteral("Chat session manager is not available");
LOG_MESSAGE(error); auto &chatAssistantSettings = Settings::chatAssistantSettings();
emit errorOccurred(error);
return; auto providerName = Settings::generalSettings().caProvider();
} auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!m_history) {
const QString error = QStringLiteral("Chat history is not available"); if (!provider) {
LOG_MESSAGE(error); LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
emit errorOccurred(error);
return; return;
} }
QString sessionError; auto templateName = Settings::generalSettings().caTemplate();
Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError); auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!session) {
const QString error = sessionError.isEmpty() if (!promptTemplate) {
? QStringLiteral("No chat agent selected") LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
: sessionError;
LOG_MESSAGE(error);
emit errorOccurred(error);
return; return;
} }
auto *client = session->client(); PluginLLMCore::ContextData context;
if (!client) {
const QString error = QStringLiteral("Chat agent has no live client");
LOG_MESSAGE(error);
m_sessionManager->removeSession(session);
emit errorOccurred(error);
return;
}
auto *project = ProjectExplorer::ProjectManager::startupProject(); const bool isToolsEnabled = useTools;
Templates::ContextRenderer::Bindings bindings;
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
bindings.homeDir = QDir::homePath();
bindings.roleId = m_activeRoleId;
session->setContextBindings(bindings);
const QString chatFilePath = m_chatFilePath; if (chatAssistantSettings.useSystemPrompt()) {
session->setContentLoader([chatFilePath](const QString &storedPath) { QString systemPrompt = chatAssistantSettings.systemPrompt();
return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath);
});
m_sessionManager->toolContributors().contribute(client->tools()); const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations()); if (!lastRoleId.isEmpty()) {
client->setTransferTimeout( const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000)); if (!role.id.isEmpty())
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
const QString chatContext = buildChatContextLayer(message, linkedFiles);
if (!chatContext.isEmpty())
session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<LLMQore::TextContent>(message));
for (const auto &attachment : storedAttachments) {
blocks.push_back(
std::make_unique<StoredAttachmentContent>(attachment.filename, attachment.content));
}
if (!storedImages.isEmpty() && session->supportsImages()) {
for (const auto &image : storedImages) {
blocks.push_back(std::make_unique<StoredImageContent>(
image.fileName, image.storedPath, image.mediaType));
} }
} else if (!storedImages.isEmpty() && !session->supportsImages()) {
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored") auto project = PluginLLMCore::RulesLoader::getActiveProject();
.arg(m_activeAgent)
.arg(storedImages.size())); if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
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
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
}
} else {
systemPrompt += QString("\n# No active project in IDE");
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
} }
if (!m_chatFilePath.isEmpty()) { QVector<PluginLLMCore::Message> messages;
if (auto *todoTool for (const auto &msg : m_chatModel->getChatHistory()) {
= qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) { if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
todoTool->setCurrentSessionId(m_chatFilePath); continue;
} }
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
client->tools()->tool("read_original_history"))) { PluginLLMCore::Message apiMessage;
historyTool->setCurrentSessionId(m_chatFilePath); apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
apiMessage.content += "\n\nAttached files:";
for (const auto &attachment : msg.attachments) {
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
if (!fileContent.isEmpty()) {
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
.arg(attachment.filename, decodedContent);
}
}
} }
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) {
apiMessage.images = apiImages;
}
}
messages.append(apiMessage);
} }
connect(session, &Session::event, this, [this, session](const QodeAssist::ResponseEvent &ev) { if (!imageFiles.isEmpty()
onSessionEvent(session, ev); && !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
}); LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size())));
}
context.history = messages;
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
provider->prepareRequest(
payload,
promptTemplate,
context,
PluginLLMCore::RequestType::Chat,
useTools,
useThinking);
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
connect( connect(
session, &Session::finished, this, provider->client(),
[this](const LLMQore::RequestID &id, const QString &) { onSessionFinished(id); }); &::LLMQore::BaseClient::chunkReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect( connect(
session, &Session::failed, this, provider->client(),
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) { &::LLMQore::BaseClient::requestCompleted,
onSessionFailed(id, error); this,
}); &ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolResultReady,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
const LLMQore::RequestID requestId = session->send(std::move(blocks)); const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
if (requestId.isEmpty()) { const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2") : promptTemplate->endpoint();
.arg(m_activeAgent, session->lastError().message); auto requestId
LOG_MESSAGE(error); = provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
m_sessionManager->removeSession(session); QJsonObject request{{"id", requestId}};
emit errorOccurred(error);
return;
}
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session}; m_activeRequests[requestId] = {request, provider};
emit requestStarted(requestId); emit requestStarted(requestId);
}
QString ClientInterface::requestIdForSession(Session *session) const if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
{ && provider->toolsManager()) {
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) { if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
if (it.value().session == session) provider->toolsManager()->tool("todo_tool"))) {
return it.key(); todoTool->setCurrentSessionId(m_chatFilePath);
}
return {};
}
void ClientInterface::onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev)
{
if (ev.kind() != ResponseEvent::Kind::Usage)
return;
const auto *usage = ev.as<ResponseEvents::Usage>();
if (!usage)
return;
const QString requestId = requestIdForSession(session);
if (!requestId.isEmpty()) {
m_chatModel->setMessageUsage(
requestId,
usage->inputTokens,
usage->outputTokens,
usage->cachedTokens,
usage->reasoningTokens);
}
emit messageUsageReceived(
usage->inputTokens, usage->outputTokens, usage->cachedTokens, usage->reasoningTokens);
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(usage->inputTokens)
.arg(usage->outputTokens)
.arg(usage->cachedTokens)
.arg(usage->reasoningTokens));
}
void ClientInterface::onSessionFinished(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
QString applyError;
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
emit messageReceivedCompletely();
m_activeRequests.erase(it);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
}
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
emit errorOccurred(error.message);
m_activeRequests.erase(it);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
}
QString ClientInterface::buildChatContextLayer(
const QString &message, const QList<QString> &linkedFiles) const
{
QString context;
auto *project = ProjectExplorer::ProjectManager::startupProject();
if (project) {
context += QString("# Active project: %1").arg(project->displayName());
context += QString(
"\n# Project source root: %1"
"\n# All new source files, headers, QML and CMake edits MUST be "
"created or modified under this directory. Use absolute paths "
"rooted here, or project-relative paths.")
.arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) {
context += QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(buildConfig->buildDirectory().toUrlishString());
}
}
} else {
context += QString("# No active project in IDE");
}
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
projectSkillDirs
= Settings::SkillsSettings::splitLines(projectSettings.projectSkillDirs());
}
m_skillsManager->configure(
project ? project->projectDirectory().toFSPathString() : QString(),
Settings::SkillsSettings::splitPaths(Settings::skillsSettings().globalSkillRoots()),
projectSkillDirs);
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
if (!alwaysOnSkills.isEmpty())
context += QString("\n\n") + alwaysOnSkills;
const QString skillsCatalog = m_skillsManager->catalogText();
if (!skillsCatalog.isEmpty())
context += QString("\n\n") + skillsCatalog;
static const QRegularExpression skillCommand(
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
QStringList invokedSkillNames;
auto skillMatch = skillCommand.globalMatch(message);
while (skillMatch.hasNext()) {
const QString skillName = skillMatch.next().captured(1);
if (invokedSkillNames.contains(skillName))
continue;
const auto invokedSkill = m_skillsManager->findByName(skillName);
if (invokedSkill && !invokedSkill->body.isEmpty()) {
invokedSkillNames << skillName;
context += QString("\n\n# Invoked Skill: %1\n\n%2")
.arg(invokedSkill->name, invokedSkill->body);
}
} }
} }
if (!linkedFiles.isEmpty()) {
context += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles)
context += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
return context;
} }
void ClientInterface::clearMessages() void ClientInterface::clearMessages()
{ {
if (m_history) const auto providerName = Settings::generalSettings().caProvider();
m_history->clear(); auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider && !m_chatFilePath.isEmpty()
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
m_chatModel->clear();
} }
void ClientInterface::cancelRequest() void ClientInterface::cancelRequest()
{ {
const auto requests = m_activeRequests; QSet<PluginLLMCore::Provider *> providers;
m_activeRequests.clear(); for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
for (auto it = requests.begin(); it != requests.end(); ++it) { providers.insert(it.value().provider);
Session *session = it.value().session; }
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
} }
LOG_MESSAGE("All chat requests cancelled and state cleared"); for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear();
m_accumulatedResponses.clear();
m_awaitingContinuation.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
{
const auto message = response.trimmed();
if (!message.isEmpty()) {
QString messageId = request["id"].toString();
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
}
} }
QString ClientInterface::getCurrentFileContext() const QString ClientInterface::getCurrentFileContext() const
@@ -468,11 +380,136 @@ QString ClientInterface::getCurrentFileContext() const
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content); return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
} }
QString ClientInterface::getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
}
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const Context::ContextManager *ClientInterface::contextManager() const
{ {
return m_contextManager; return m_contextManager;
} }
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value();
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
}
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
QString applyError;
bool applySuccess
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
if (!applySuccess) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
LOG_MESSAGE(
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
+ ": " + finalText);
emit messageReceivedCompletely();
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
emit errorOccurred(error);
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
return;
}
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
if (thinking.isEmpty()) {
m_chatModel->addRedactedThinkingBlock(requestId, signature);
} else {
m_chatModel->addThinkingBlock(requestId, thinking, signature);
}
}
void ClientInterface::handleToolExecutionStarted(
const QString &requestId, const QString &toolId, const QString &toolName)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
return;
}
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
m_awaitingContinuation.insert(requestId);
}
void ClientInterface::handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
return;
}
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
}
bool ClientInterface::isImageFile(const QString &filePath) const bool ClientInterface::isImageFile(const QString &filePath) const
{ {
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"}; static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
@@ -520,8 +557,46 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
return imageData.toBase64(); return imageData.toBase64();
} }
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const
{
QVector<PluginLLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) {
QString base64Data
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
if (base64Data.isEmpty()) {
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
continue;
}
PluginLLMCore::ImageAttachment apiImage;
apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false;
apiImages.append(apiImage);
}
return apiImages;
}
void ClientInterface::setChatFilePath(const QString &filePath) void ClientInterface::setChatFilePath(const QString &filePath)
{ {
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
}
m_chatFilePath = filePath; m_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath); m_chatModel->setChatFilePath(filePath);
} }

View File

@@ -1,29 +1,18 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QPointer> #include <QSet>
#include <QString> #include <QString>
#include <QVector>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include <ErrorInfo.hpp> #include "Provider.hpp"
#include <LLMQore/BaseClient.hpp> #include "pluginllmcore/IPromptProvider.hpp"
#include <ResponseEvent.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ClientInterface : public QObject class ClientInterface : public QObject
@@ -31,19 +20,16 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr); explicit ClientInterface(
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager);
void setSessionManager(SessionManager *sessionManager);
void setHistory(ConversationHistory *history);
void setActiveAgent(const QString &agentName);
void setActiveRole(const QString &roleId);
void sendMessage( void sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments = {}, const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {}); const QList<QString> &linkedFiles = {},
bool useTools = false,
bool useThinking = false);
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
@@ -56,38 +42,45 @@ signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely(); void messageReceivedCompletely();
void requestStarted(const QString &requestId); void requestStarted(const QString &requestId);
void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens); private slots:
void handlePartialResponse(const QString &requestId, const QString &partialText);
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
void handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void handleToolExecutionStarted(
const QString &requestId, const QString &toolId, const QString &toolName);
void handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput);
private: private:
void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev); void handleLLMResponse(const QString &response, const QJsonObject &request);
void onSessionFinished(const QString &requestId);
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString buildChatContextLayer( QString getSystemPromptWithLinkedFiles(
const QString &message, const QList<QString> &linkedFiles) const; const QString &basePrompt, const QList<QString> &linkedFiles) const;
QString requestIdForSession(Session *session) const;
bool isImageFile(const QString &filePath) const; bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
QPointer<Session> session; PluginLLMCore::Provider *provider;
}; };
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QPointer<ConversationHistory> m_history;
Skills::SkillsManager *m_skillsManager = nullptr;
QPointer<SessionManager> m_sessionManager;
QString m_activeAgent;
QString m_activeRoleId;
QString m_chatFilePath; QString m_chatFilePath;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
QSet<QString> m_awaitingContinuation;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,278 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FileEditController.hpp"
#include <QJsonDocument>
#include <QJsonObject>
#include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h>
#include "Logger.hpp"
#include "context/ChangesManager.h"
namespace QodeAssist::Chat {
FileEditController::FileEditController(QObject *parent)
: QObject(parent)
{
auto &changes = Context::ChangesManager::instance();
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
updateStats();
});
connect(&changes, &Context::ChangesManager::fileEditApplied, this, [this](const QString &) {
updateStats();
});
connect(&changes, &Context::ChangesManager::fileEditRejected, this, [this](const QString &) {
updateStats();
});
connect(&changes, &Context::ChangesManager::fileEditUndone, this, [this](const QString &) {
updateStats();
});
connect(&changes, &Context::ChangesManager::fileEditArchived, this, [this](const QString &) {
updateStats();
});
}
void FileEditController::setCurrentRequestId(const QString &requestId)
{
if (!m_currentRequestId.isEmpty()) {
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentRequestId));
}
m_currentRequestId = requestId;
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
updateStats();
}
void FileEditController::clearCurrentRequestId()
{
m_currentRequestId.clear();
updateStats();
}
int FileEditController::totalEdits() const
{
return m_totalEdits;
}
int FileEditController::appliedEdits() const
{
return m_appliedEdits;
}
int FileEditController::pendingEdits() const
{
return m_pendingEdits;
}
int FileEditController::rejectedEdits() const
{
return m_rejectedEdits;
}
void FileEditController::applyFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
emit infoMessage(QString("File edit applied successfully"));
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
edit.statusMessage.isEmpty()
? QString("Failed to apply file edit")
: QString("Failed to apply file edit: %1").arg(edit.statusMessage));
}
}
void FileEditController::rejectFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
emit infoMessage(QString("File edit rejected"));
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
edit.statusMessage.isEmpty()
? QString("Failed to reject file edit")
: QString("Failed to reject file edit: %1").arg(edit.statusMessage));
}
}
void FileEditController::undoFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
emit infoMessage(QString("File edit undone successfully"));
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred(
edit.statusMessage.isEmpty()
? QString("Failed to undo file edit")
: QString("Failed to undo file edit: %1").arg(edit.statusMessage));
}
}
void FileEditController::openFileEditInEditor(const QString &editId)
{
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (edit.editId.isEmpty()) {
emit errorOccurred(QString("File edit not found: %1").arg(editId));
return;
}
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
if (!editor) {
emit errorOccurred(QString("Failed to open file in editor: %1").arg(edit.filePath));
return;
}
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
if (textEditor && textEditor->editorWidget()) {
QTextDocument *doc = textEditor->editorWidget()->document();
if (doc) {
QString currentContent = doc->toPlainText();
int position = -1;
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
position = currentContent.indexOf(edit.newContent);
} else if (!edit.oldContent.isEmpty()) {
position = currentContent.indexOf(edit.oldContent);
}
if (position >= 0) {
QTextCursor cursor(doc);
cursor.setPosition(position);
textEditor->editorWidget()->setTextCursor(cursor);
textEditor->editorWidget()->centerCursor();
}
}
}
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
}
void FileEditController::applyAllForCurrentMessage()
{
if (m_currentRequestId.isEmpty()) {
emit errorOccurred(QString("No active message with file edits"));
return;
}
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentRequestId));
QString errorMsg;
bool success = Context::ChangesManager::instance()
.reapplyAllEditsForRequest(m_currentRequestId, &errorMsg);
if (success) {
emit infoMessage(QString("All file edits applied successfully"));
} else {
emit errorOccurred(
errorMsg.isEmpty()
? QString("Failed to apply some file edits")
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
}
updateStats();
}
void FileEditController::undoAllForCurrentMessage()
{
if (m_currentRequestId.isEmpty()) {
emit errorOccurred(QString("No active message with file edits"));
return;
}
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentRequestId));
QString errorMsg;
bool success = Context::ChangesManager::instance()
.undoAllEditsForRequest(m_currentRequestId, &errorMsg);
if (success) {
emit infoMessage(QString("All file edits undone successfully"));
} else {
emit errorOccurred(
errorMsg.isEmpty()
? QString("Failed to undo some file edits")
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
}
updateStats();
}
void FileEditController::updateStats()
{
if (m_currentRequestId.isEmpty()) {
if (m_totalEdits != 0 || m_appliedEdits != 0 || m_pendingEdits != 0
|| m_rejectedEdits != 0) {
m_totalEdits = 0;
m_appliedEdits = 0;
m_pendingEdits = 0;
m_rejectedEdits = 0;
emit statsChanged();
}
return;
}
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
int total = edits.size();
int applied = 0;
int pending = 0;
int rejected = 0;
for (const auto &edit : edits) {
switch (edit.status) {
case Context::ChangesManager::Applied:
applied++;
break;
case Context::ChangesManager::Pending:
pending++;
break;
case Context::ChangesManager::Rejected:
rejected++;
break;
case Context::ChangesManager::Archived:
total--;
break;
}
}
bool changed = false;
if (m_totalEdits != total) {
m_totalEdits = total;
changed = true;
}
if (m_appliedEdits != applied) {
m_appliedEdits = applied;
changed = true;
}
if (m_pendingEdits != pending) {
m_pendingEdits = pending;
changed = true;
}
if (m_rejectedEdits != rejected) {
m_rejectedEdits = rejected;
changed = true;
}
if (changed) {
LOG_MESSAGE(
QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
.arg(total)
.arg(applied)
.arg(pending)
.arg(rejected));
emit statsChanged();
}
}
} // namespace QodeAssist::Chat

View File

@@ -1,49 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QString>
namespace QodeAssist::Chat {
class FileEditController : public QObject
{
Q_OBJECT
public:
explicit FileEditController(QObject *parent = nullptr);
void setCurrentRequestId(const QString &requestId);
void clearCurrentRequestId();
int totalEdits() const;
int appliedEdits() const;
int pendingEdits() const;
int rejectedEdits() const;
void applyFileEdit(const QString &editId);
void rejectFileEdit(const QString &editId);
void undoFileEdit(const QString &editId);
void openFileEditInEditor(const QString &editId);
void applyAllForCurrentMessage();
void undoAllForCurrentMessage();
void updateStats();
signals:
void statsChanged();
void infoMessage(const QString &message);
void errorOccurred(const QString &error);
private:
QString m_currentRequestId;
int m_totalEdits{0};
int m_appliedEdits{0};
int m_pendingEdits{0};
int m_rejectedEdits{0};
};
} // namespace QodeAssist::Chat

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FileItem.hpp" #include "FileItem.hpp"

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2026 Petr Mironychev // Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FileMentionItem.hpp" #include "FileMentionItem.hpp"

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2026 Petr Mironychev // Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,142 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "InputTokenCounter.hpp"
#include <algorithm>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp"
#include "Logger.hpp"
#include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
namespace QodeAssist::Chat {
InputTokenCounter::InputTokenCounter(
ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
: QObject(parent)
, m_history(history)
, m_contextManager(contextManager)
{
auto &settings = Settings::chatAssistantSettings();
connect(
&settings.useSystemPrompt,
&Utils::BaseAspect::changed,
this,
&InputTokenCounter::recompute);
connect(
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
connect(
&settings.enableChatTools,
&Utils::BaseAspect::changed,
this,
&InputTokenCounter::recompute);
recompute();
}
int InputTokenCounter::inputTokens() const
{
return m_inputTokens;
}
void InputTokenCounter::setMessage(const QString &message)
{
m_messageTokens = Context::TokenUtils::estimateTokens(message);
recompute();
}
void InputTokenCounter::setAttachments(const QStringList &attachments)
{
m_attachments = attachments;
recompute();
}
void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
{
m_linkedFiles = linkedFiles;
recompute();
}
void InputTokenCounter::recompute()
{
int inputTokens = m_messageTokens;
auto &settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
}
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
int imageTokens = 0;
for (const QString &p : paths) {
if (Context::TokenUtils::isImageFilePath(p))
imageTokens += Context::TokenUtils::estimateImageAttachmentTokens(p);
else
textPaths.append(p);
}
return imageTokens;
};
if (!m_attachments.isEmpty()) {
QStringList textPaths;
inputTokens += splitImageEstimate(m_attachments, textPaths);
if (!textPaths.isEmpty()) {
auto attachFiles = m_contextManager->getContentFiles(textPaths);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
}
}
if (!m_linkedFiles.isEmpty()) {
QStringList textPaths;
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
if (!textPaths.isEmpty()) {
auto linkFiles = m_contextManager->getContentFiles(textPaths);
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
}
}
if (m_history) {
for (const auto &message : m_history->messages()) {
inputTokens += Context::TokenUtils::estimateTokens(message.text());
inputTokens += 4; // + role
}
}
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
emit inputTokensChanged();
}
void InputTokenCounter::recordSent()
{
m_lastSentEstimate = m_calibrationFactor > 0.0
? static_cast<int>(m_inputTokens / m_calibrationFactor)
: m_inputTokens;
}
void InputTokenCounter::recordServerUsage(int promptTokens)
{
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
return;
const double rawFactor
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped;
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
.arg(promptTokens)
.arg(m_lastSentEstimate)
.arg(rawFactor, 0, 'f', 3)
.arg(m_calibrationFactor, 0, 'f', 3));
recompute();
}
} // namespace QodeAssist::Chat

View File

@@ -1,55 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QStringList>
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Context {
class ContextManager;
}
namespace QodeAssist::Chat {
class InputTokenCounter : public QObject
{
Q_OBJECT
public:
InputTokenCounter(
ConversationHistory *history,
Context::ContextManager *contextManager,
QObject *parent = nullptr);
int inputTokens() const;
void setMessage(const QString &message);
void setAttachments(const QStringList &attachments);
void setLinkedFiles(const QStringList &linkedFiles);
void recompute();
void recordSent();
void recordServerUsage(int promptTokens);
signals:
void inputTokensChanged();
private:
ConversationHistory *m_history;
Context::ContextManager *m_contextManager;
QStringList m_attachments;
QStringList m_linkedFiles;
int m_messageTokens{0};
int m_inputTokens{0};
int m_lastSentEstimate{0};
double m_calibrationFactor{1.0};
};
} // namespace QodeAssist::Chat

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,68 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "SessionFileRegistry.hpp"
#include <utility>
#include <QFileInfo>
namespace QodeAssist::Chat {
SessionFileRegistry::SessionFileRegistry(QObject *parent)
: QObject(parent)
{}
bool SessionFileRegistry::isLocked(const QString &path) const
{
return !path.isEmpty() && m_lockedPaths.contains(path);
}
bool SessionFileRegistry::lock(const QString &path)
{
if (path.isEmpty() || m_lockedPaths.contains(path)) {
return false;
}
m_lockedPaths.insert(path);
return true;
}
void SessionFileRegistry::release(const QString &path)
{
m_lockedPaths.remove(path);
}
void SessionFileRegistry::setPendingChatFile(const QString &path)
{
m_pendingChatFile = path;
}
QString SessionFileRegistry::takePendingChatFile()
{
return std::exchange(m_pendingChatFile, QString{});
}
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
{
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
return desiredPath;
}
const QFileInfo info(desiredPath);
const QString dir = info.path();
const QString base = info.completeBaseName();
const QString suffix = info.suffix();
for (int counter = 2;; ++counter) {
QString candidate = dir + '/' + base + '_' + QString::number(counter);
if (!suffix.isEmpty()) {
candidate += '.' + suffix;
}
if (!m_lockedPaths.contains(candidate)) {
return candidate;
}
}
}
} // namespace QodeAssist::Chat

View File

@@ -1,39 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QSet>
#include <QString>
namespace QodeAssist::Chat {
// Shared registry of chat session (autosave) file paths that are currently held by a live
// chat instance. Lets every chat view — bottom pane, navigation panel, editor split — claim
// a unique history file so two sessions never autosave into the same path.
class SessionFileRegistry : public QObject
{
Q_OBJECT
public:
explicit SessionFileRegistry(QObject *parent = nullptr);
bool isLocked(const QString &path) const;
bool lock(const QString &path);
void release(const QString &path);
QString uniqueFreePath(const QString &desiredPath) const;
// Handoff slot for relocating a live chat between hosts (split <-> window): the source
// chat stores its history file here, the freshly created host picks it up exactly once.
void setPendingChatFile(const QString &path);
QString takePendingChatFile();
private:
QSet<QString> m_lockedPaths;
QString m_pendingChatFile;
};
} // namespace QodeAssist::Chat

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8Z" fill="black"/>
<rect x="11" y="5" width="2" height="9" rx="0.5" fill="black"/>
<rect x="7.5" y="8.5" width="9" height="2" rx="0.5" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

View File

@@ -1,17 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_74_52)">
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<path d="M44 0H0V44H44V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_74_52)">
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
</g>
</g>
<defs>
<clipPath id="clip0_74_52">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 943 B

View File

@@ -1,6 +1,17 @@
<svg width="44" height="44" viewBox="0 0 44 44" 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">
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round"> <g clip-path="url(#clip0_74_52)">
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="black"/> <mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="none"/> <path d="M44 0H0V44H44V0Z" fill="white"/>
</g> </mask>
<g mask="url(#mask0_74_52)">
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
</g>
</g>
<defs>
<clipPath id="clip0_74_52">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 943 B

View File

@@ -1,6 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round">
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="none"/>
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="black"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 348 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3L22 20H2L12 3Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
<path d="M12 10V14" stroke="black" stroke-width="2" stroke-linecap="round"/>
<path d="M12 17H12.01" stroke="black" stroke-width="2.4" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 350 B

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -20,9 +19,6 @@ ChatRootView {
colorGroup: SystemPalette.Active colorGroup: SystemPalette.Active
} }
property bool hasActiveError: false
readonly property color errorColor: "#d32f2f"
palette { palette {
window: sysPalette.window window: sysPalette.window
windowText: sysPalette.windowText windowText: sysPalette.windowText
@@ -91,28 +87,11 @@ ChatRootView {
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + 10 Layout.preferredHeight: childrenRect.height + 10
isInEditor: root.isInEditor
saveButton.onClicked: root.showSaveDialog() saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog() loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat() clearButton.onClicked: root.clearChat()
newChatButton.onClicked: root.requestNewChat()
tokensBadge { tokensBadge {
readonly property int sessionPrompt: root.chatModel.sessionPromptTokens || 0 text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
readonly property int sessionCompletion: root.chatModel.sessionCompletionTokens || 0
readonly property int sessionCached: root.chatModel.sessionCachedPromptTokens || 0
text: sessionCached > 0
? qsTr("next ~%1 · session ↑%2 ↓%3 ↻%4")
.arg(root.inputTokensCount)
.arg(sessionPrompt)
.arg(sessionCompletion)
.arg(sessionCached)
: qsTr("next ~%1 · session ↑%2 ↓%3")
.arg(root.inputTokensCount)
.arg(sessionPrompt)
.arg(sessionCompletion)
ToolTip.text: sessionCached > 0
? qsTr("next request (estimate) · session prompt ↑ / completion ↓ / cached ↻ (provider cache hits)")
: qsTr("next request (estimate) · session prompt ↑ / completion ↓")
} }
recentPath { recentPath {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved") text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
@@ -124,91 +103,61 @@ ChatRootView {
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
} }
relocateButton { toolsButton {
icon.source: (typeof _chatview !== 'undefined') checked: root.useTools
? "qrc:/qt/qml/ChatView/icons/open-in-editor.svg" onCheckedChanged: {
: "qrc:/qt/qml/ChatView/icons/open-in-window.svg" root.useTools = toolsButton.checked
onClicked: { }
if (typeof _chatview !== 'undefined') }
root.relocateToSplit() thinkingMode {
else checked: root.useThinking
root.relocateToWindow() enabled: root.isThinkingSupport
onCheckedChanged: {
root.useThinking = thinkingMode.checked
} }
} }
relocateTooltip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window")
settingsButton.onClicked: root.openSettings() settingsButton.onClicked: root.openSettings()
agentSelector { configSelector {
model: root.availableChatAgents model: root.availableConfigurations
displayText: root.currentChatAgent displayText: root.currentConfiguration
onActivated: function(index) { onActivated: function(index) {
root.currentChatAgent = root.availableChatAgents[index] if (index > 0) {
root.applyConfiguration(root.availableConfigurations[index])
}
} }
Component.onCompleted: root.loadAvailableChatAgents()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableChatAgents() root.loadAvailableConfigurations()
} }
} }
roleSelector { roleSelector {
model: root.availableRoles model: root.availableAgentRoles
displayText: root.currentRole displayText: root.currentAgentRole
onActivated: function(index) { onActivated: function(index) {
root.currentRole = root.availableRoles[index] root.applyAgentRole(root.availableAgentRoles[index])
} }
Component.onCompleted: root.loadAvailableRoles()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableRoles() root.loadAvailableAgentRoles()
} }
} }
} }
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 2
MessageNavigator {
id: messageNavigator
Layout.preferredWidth: 16
Layout.fillHeight: true
Layout.topMargin: 4
Layout.bottomMargin: 4
chatModel: root.chatModel
onMessageClicked: function(messageIndex) {
chatListView.userScrolledUp = true
chatListView.positionViewAtIndex(messageIndex, ListView.Beginning)
}
}
ListView { ListView {
id: chatListView id: chatListView
property bool userScrolledUp: false property bool userScrolledUp: false
function syncNavigatorCurrent() {
const top = indexAt(10, contentY + 4)
messageNavigator.updateCurrentFromModelIndex(top)
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
leftMargin: 3 leftMargin: 5
model: root.chatModel model: root.chatModel
clip: true clip: true
spacing: 0 spacing: 0
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000 cacheBuffer: 2000
onContentYChanged: Qt.callLater(syncNavigatorCurrent)
onMovingChanged: { onMovingChanged: {
if (moving) { if (moving) {
userScrolledUp = !atYEnd userScrolledUp = !atYEnd
@@ -295,7 +244,6 @@ ChatRootView {
if (!userScrolledUp) { if (!userScrolledUp) {
root.scrollToBottom() root.scrollToBottom()
} }
Qt.callLater(syncNavigatorCurrent)
} }
onContentHeightChanged: { onContentHeightChanged: {
@@ -311,7 +259,6 @@ ChatRootView {
id: chatItemInstance id: chatItemInstance
width: parent.width width: parent.width
chatViewport: chatListView
msgModel: root.chatModel.processMessageContent(model.content) msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments messageAttachments: model.attachments
messageImages: model.images messageImages: model.images
@@ -323,10 +270,6 @@ ChatRootView {
codeFontSize: root.codeFontSize codeFontSize: root.codeFontSize
textFontSize: root.textFontSize textFontSize: root.textFontSize
textFormat: root.textFormat textFormat: root.textFormat
promptTokens: model.promptTokens || 0
completionTokens: model.completionTokens || 0
cachedPromptTokens: model.cachedPromptTokens || 0
reasoningTokens: model.reasoningTokens || 0
onResetChatToMessage: function(idx) { onResetChatToMessage: function(idx) {
messageInput.text = model.content messageInput.text = model.content
@@ -391,7 +334,6 @@ ChatRootView {
} }
} }
} }
}
ScrollView { ScrollView {
id: view id: view
@@ -403,10 +345,11 @@ ChatRootView {
QQC.TextArea { QQC.TextArea {
id: messageInput id: messageInput
placeholderText: qsTr("Type your message here... (%1 to send)").arg(root.sendShortcutText) placeholderText: Qt.platform.os === "osx"
? qsTr("Type your message here... (⌘+↩ to send)")
: qsTr("Type your message here... (Ctrl+Enter to send)")
placeholderTextColor: palette.mid placeholderTextColor: palette.mid
color: palette.text color: palette.text
wrapMode: TextArea.Wrap
background: Rectangle { background: Rectangle {
radius: 2 radius: 2
color: palette.base color: palette.base
@@ -429,31 +372,15 @@ ChatRootView {
root.calculateMessageTokensCount(messageInput.text) root.calculateMessageTokensCount(messageInput.text)
var cursorPos = messageInput.cursorPosition var cursorPos = messageInput.cursorPosition
var textBefore = messageInput.text.substring(0, cursorPos) var textBefore = messageInput.text.substring(0, cursorPos)
var atIndex = textBefore.lastIndexOf('@') var atIndex = textBefore.lastIndexOf('@')
if (atIndex >= 0) { if (atIndex >= 0) {
var query = textBefore.substring(atIndex + 1) var query = textBefore.substring(atIndex + 1)
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) { if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
fileMentionPopup.updateSearch(query) fileMentionPopup.updateSearch(query)
skillCommandPopup.dismiss()
return return
} }
} }
fileMentionPopup.dismiss() fileMentionPopup.dismiss()
const slashIndex = textBefore.lastIndexOf('/')
if (slashIndex >= 0) {
const beforeSlash = slashIndex === 0
? ' '
: textBefore.charAt(slashIndex - 1)
const skillQuery = textBefore.substring(slashIndex + 1)
if ((beforeSlash === ' ' || beforeSlash === '\n')
&& /^[a-z0-9-]*$/.test(skillQuery)) {
skillCommandPopup.updateSearch(skillQuery)
return
}
}
skillCommandPopup.dismiss()
} }
Keys.onPressed: function(event) { Keys.onPressed: function(event) {
@@ -471,23 +398,6 @@ ChatRootView {
fileMentionPopup.dismiss() fileMentionPopup.dismiss()
event.accepted = true event.accepted = true
} }
} else if (skillCommandPopup.visible) {
if (event.key === Qt.Key_Down) {
skillCommandPopup.moveDown()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
skillCommandPopup.moveUp()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.applySkillSelection()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
skillCommandPopup.dismiss()
event.accepted = true
}
} else if (root.isSendShortcut(event.key, event.modifiers)) {
root.sendChatMessage()
event.accepted = true
} }
} }
@@ -580,21 +490,13 @@ ChatRootView {
Layout.preferredHeight: 40 Layout.preferredHeight: 40
isCompressing: root.isCompressing isCompressing: root.isCompressing
isProcessing: root.isRequestInProgress
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage() sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest() : root.cancelRequest()
sendButton.icon.source: root.isRequestInProgress sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
? "" : "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
: (root.hasActiveError ? "qrc:/qt/qml/ChatView/icons/warning-icon.svg" sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
: "qrc:/qt/qml/ChatView/icons/chat-icon.svg") sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
sendButton.text: root.isRequestInProgress ? qsTr("Stop") : qsTr("Send") : qsTr("Stop")
sendButton.accentColor: (root.hasActiveError && !root.isRequestInProgress)
? root.errorColor : "transparent"
sendButtonTooltip.text: root.isRequestInProgress
? qsTr("Stop")
: (root.hasActiveError
? root.lastErrorMessage
: qsTr("Send message to LLM %1").arg(root.sendShortcutText))
compressButton.onClicked: compressConfirmDialog.open() compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression() cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles { syncOpenFiles {
@@ -607,6 +509,15 @@ ChatRootView {
} }
} }
Shortcut {
id: sendMessageShortcut
sequences: ["Ctrl+Return", "Ctrl+Enter"]
context: Qt.WindowShortcut
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
onActivated: root.sendChatMessage()
}
function clearChat() { function clearChat() {
root.clearMessages() root.clearMessages()
root.clearAttachmentFiles() root.clearAttachmentFiles()
@@ -617,31 +528,6 @@ ChatRootView {
Qt.callLater(chatListView.positionViewAtEnd) Qt.callLater(chatListView.positionViewAtEnd)
} }
function focusInput() {
messageInput.forceActiveFocus()
}
property Item focusGuard: Window.activeFocusItem
onFocusGuardChanged: Qt.callLater(returnFocusToInputIfNeeded)
function returnFocusToInputIfNeeded() {
var item = Window.activeFocusItem
if (!item || item === messageInput)
return
if (item.cursorVisible !== undefined || item.selectByMouse !== undefined)
return
if (item.popup !== undefined)
return
var p = item
while (p) {
if (p === root) {
messageInput.forceActiveFocus()
return
}
p = p.parent
}
}
function applyMentionSelection() { function applyMentionSelection() {
var result = fileMentionPopup.applyCurrentSelection( var result = fileMentionPopup.applyCurrentSelection(
messageInput.text, messageInput.cursorPosition, root.useTools) messageInput.text, messageInput.cursorPosition, root.useTools)
@@ -651,25 +537,7 @@ ChatRootView {
} }
} }
function applySkillSelection() {
const name = skillCommandPopup.currentName()
if (name === "")
return
const cursorPos = messageInput.cursorPosition
const textBefore = messageInput.text.substring(0, cursorPos)
const slashIndex = textBefore.lastIndexOf('/')
if (slashIndex < 0)
return
const before = messageInput.text.substring(0, slashIndex)
const after = messageInput.text.substring(cursorPos)
const token = '/' + name + ' '
messageInput.text = before + token + after
messageInput.cursorPosition = before.length + token.length
skillCommandPopup.dismiss()
}
function sendChatMessage() { function sendChatMessage() {
root.hasActiveError = false
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text)) root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = "" messageInput.text = ""
fileMentionPopup.clearMentions() fileMentionPopup.clearMentions()
@@ -692,122 +560,13 @@ ChatRootView {
onAccepted: root.compressCurrentChat() onAccepted: root.compressCurrentChat()
} }
Rectangle { Toast {
id: errorBanner id: errorToast
z: 1000 z: 1000
visible: root.hasActiveError && root.lastErrorMessage.length > 0
width: parent.width / 2 color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
anchors.right: parent.right border.color: Qt.darker(infoToast.color, 1.3)
anchors.bottom: parent.bottom toastTextColor: "#FFFFFF"
anchors.rightMargin: 10
anchors.bottomMargin: bottomBar.height + 48
height: visible ? errorRow.implicitHeight + 12 : 0
color: Qt.rgba(0.83, 0.18, 0.18, 0.96)
radius: 6
border.color: Qt.darker(color, 1.3)
border.width: 1
RowLayout {
id: errorRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 6
spacing: 8
TextEdit {
Layout.fillWidth: true
text: root.lastErrorMessage
color: "#FFFFFF"
font.pixelSize: 12
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
selectionColor: Qt.darker(errorBanner.color, 1.3)
}
Rectangle {
id: copyErrorButton
property bool copied: false
Layout.alignment: Qt.AlignTop
implicitWidth: copyErrorLabel.implicitWidth + 18
implicitHeight: 22
radius: 4
color: copyErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28)
: Qt.rgba(1, 1, 1, 0.16)
border.color: Qt.rgba(1, 1, 1, 0.45)
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Text {
id: copyErrorLabel
anchors.centerIn: parent
text: copyErrorButton.copied ? qsTr("Copied") : qsTr("Copy")
color: "#FFFFFF"
font.pixelSize: 12
}
MouseArea {
id: copyErrorMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.copyToClipboard(root.lastErrorMessage)
copyErrorButton.copied = true
copyErrorResetTimer.restart()
}
}
Timer {
id: copyErrorResetTimer
interval: 1500
onTriggered: copyErrorButton.copied = false
}
}
Rectangle {
id: closeErrorButton
Layout.alignment: Qt.AlignTop
implicitWidth: 22
implicitHeight: 22
radius: 4
color: closeErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28) : "transparent"
border.color: Qt.rgba(1, 1, 1, 0.45)
border.width: closeErrorMouse.containsMouse ? 1 : 0
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: "✕"
color: "#FFFFFF"
font.pixelSize: 12
}
MouseArea {
id: closeErrorMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hasActiveError = false
}
}
}
} }
Toast { Toast {
@@ -827,14 +586,27 @@ ChatRootView {
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 2 y: (parent.height - height) / 2
baseSystemPrompt: root.baseSystemPrompt
currentAgentRole: root.currentAgentRole
currentAgentRoleDescription: root.currentAgentRoleDescription
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
activeRules: root.activeRules
activeRulesCount: root.activeRulesCount
onOpenSettings: root.openSettings() onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
} }
Connections { Connections {
target: root target: root
function onLastErrorMessageChanged() { function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) { if (root.lastErrorMessage.length > 0) {
root.hasActiveError = true errorToast.show(root.lastErrorMessage)
} }
} }
function onLastInfoMessageChanged() { function onLastInfoMessageChanged() {
@@ -864,21 +636,7 @@ ChatRootView {
} }
} }
SkillCommandPopup {
id: skillCommandPopup
z: 999
width: Math.min(480, root.width - 20)
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
y: view.y - height - 4
skillProvider: root
onSelectionRequested: root.applySkillSelection()
}
Component.onCompleted: { Component.onCompleted: {
focusInput() messageInput.forceActiveFocus()
} }
} }

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import ChatView import ChatView
@@ -31,16 +30,10 @@ Rectangle {
property int textFontSize: Qt.application.font.pointSize property int textFontSize: Qt.application.font.pointSize
property int codeFontSize: Qt.application.font.pointSize property int codeFontSize: Qt.application.font.pointSize
property int textFormat: 0 property int textFormat: 0
property Flickable chatViewport: null
property bool isUserMessage: false property bool isUserMessage: false
property int messageIndex: -1 property int messageIndex: -1
property int promptTokens: 0
property int completionTokens: 0
property int cachedPromptTokens: 0
property int reasoningTokens: 0
signal resetChatToMessage(int index) signal resetChatToMessage(int index)
signal openFileRequested(string filePath) signal openFileRequested(string filePath)
@@ -142,39 +135,6 @@ Rectangle {
} }
} }
} }
RowLayout {
id: usageBadge
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 10
spacing: 8
visible: !root.isUserMessage
&& (root.promptTokens > 0 || root.completionTokens > 0)
Item { Layout.fillWidth: true }
Text {
text: root.cachedPromptTokens > 0
? qsTr("↑ %1 (cached %2)").arg(root.promptTokens).arg(root.cachedPromptTokens)
: qsTr("↑ %1").arg(root.promptTokens)
color: palette.placeholderText
font.pointSize: Math.max(root.textFontSize - 2, 7)
}
Text {
text: root.reasoningTokens > 0
? qsTr("↓ %1 (reasoning %2)").arg(root.completionTokens).arg(root.reasoningTokens)
: qsTr("↓ %1").arg(root.completionTokens)
color: palette.placeholderText
font.pointSize: Math.max(root.textFontSize - 2, 7)
}
Text {
text: qsTr("Σ %1").arg(root.promptTokens + root.completionTokens)
color: palette.placeholderText
font.pointSize: Math.max(root.textFontSize - 2, 7)
}
}
} }
Rectangle { Rectangle {
@@ -261,7 +221,6 @@ Rectangle {
language: itemData.language language: itemData.language
codeFontFamily: root.codeFontFamily codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize codeFontSize: root.codeFontSize
viewport: root.chatViewport
} }
component AttachmentComponent : Rectangle { component AttachmentComponent : Rectangle {
@@ -356,9 +315,10 @@ Rectangle {
smooth: true smooth: true
mipmap: true mipmap: true
QoABusyIndicator { BusyIndicator {
anchors.centerIn: parent anchors.centerIn: parent
running: imageDisplay.status === Image.Loading running: imageDisplay.status === Image.Loading
visible: running
} }
Text { Text {

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -14,7 +13,6 @@ Rectangle {
property string code: "" property string code: ""
property string language: "" property string language: ""
property bool expanded: false property bool expanded: false
property Flickable viewport: null
property alias codeFontFamily: codeText.font.family property alias codeFontFamily: codeText.font.family
property alias codeFontSize: codeText.font.pointSize property alias codeFontSize: codeText.font.pointSize
@@ -124,16 +122,7 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 5 anchors.rightMargin: 5
y: { y: 5
if (!root.expanded || !root.viewport)
return 5
const flick = root.viewport
const topInContent = root.mapToItem(flick.contentItem, 0, 0).y
const topInView = topInContent - flick.contentY
const desired = topInView < 0 ? (-topInView + 5) : 5
const maxY = Math.max(5, root.height - copyButton.height - 5)
return Math.max(5, Math.min(desired, maxY))
}
text: qsTr("Copy") text: qsTr("Copy")
onClicked: { onClicked: {

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -194,24 +193,9 @@ Rectangle {
color: root.statusColor color: root.statusColor
} }
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/open-in-code.svg"
height: 15
width: 15
}
hoverEnabled: true
onClicked: root.openInEditor(editData.edit_id)
QoAToolTip {
visible: parent.hovered
delay: 500
text: qsTr("Open file in editor and navigate to changes")
}
}
Text { Text {
id: headerText id: headerText
Layout.fillWidth: true
text: { text: {
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append") var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
if (root.oldContent.length > 0) { if (root.oldContent.length > 0) {
@@ -239,19 +223,6 @@ Rectangle {
color: palette.mid color: palette.mid
} }
Item { Layout.fillWidth: true }
}
RowLayout {
id: actionButtons
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 6
Rectangle { Rectangle {
visible: !root.isPending visible: !root.isPending
Layout.preferredWidth: badgeText.width + 12 Layout.preferredWidth: badgeText.width + 12
@@ -268,6 +239,31 @@ Rectangle {
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
} }
} }
}
Row {
id: actionButtons
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 6
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
height: 15
width: 15
}
hoverEnabled: true
onClicked: root.openInEditor(editData.edit_id)
ToolTip.visible: hovered
ToolTip.text: qsTr("Open file in editor and navigate to changes")
ToolTip.delay: 500
}
QoAButton { QoAButton {
icon { icon {

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import Qt.labs.platform as Platform import Qt.labs.platform as Platform

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import Qt.labs.platform as Platform import Qt.labs.platform as Platform

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import Qt.labs.platform as Platform import Qt.labs.platform as Platform

View File

@@ -1,11 +1,9 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Qt.labs.platform as Platform
import ChatView import ChatView
import UIControls import UIControls
@@ -54,7 +52,7 @@ Flow {
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => { onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
contextMenu.open() contextMenu.popup()
} else if (mouse.button === Qt.MiddleButton || } else if (mouse.button === Qt.MiddleButton ||
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) { (mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
root.removeFileFromListByIndex(fileItem.index) root.removeFileFromListByIndex(fileItem.index)
@@ -72,23 +70,23 @@ Flow {
} }
} }
Platform.Menu { Menu {
id: contextMenu id: contextMenu
Platform.MenuItem { MenuItem {
text: qsTr("Open in Qt Creator") text: "Open in Qt Creator"
onTriggered: fileItem.openFileInEditor() onTriggered: fileItem.openFileInEditor()
} }
Platform.MenuItem { MenuItem {
text: qsTr("Open in External Editor") text: "Open in External Editor"
onTriggered: fileItem.openFileInExternalEditor() onTriggered: fileItem.openFileInExternalEditor()
} }
Platform.MenuSeparator {} MenuSeparator {}
Platform.MenuItem { MenuItem {
text: qsTr("Remove") text: "Remove"
onTriggered: root.removeFileFromListByIndex(fileItem.index) onTriggered: root.removeFileFromListByIndex(fileItem.index)
} }
} }

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -20,8 +19,6 @@ Rectangle {
property alias cancelCompressButton: cancelCompressButtonId property alias cancelCompressButton: cancelCompressButtonId
property bool isCompressing: false property bool isCompressing: false
property bool isProcessing: false
property alias sendButtonTooltip: sendButtonTooltipId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@@ -48,12 +45,9 @@ Rectangle {
height: 15 height: 15
width: 8 width: 8
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: attachFilesId.hovered ToolTip.text: qsTr("Attach file to message")
delay: 250
text: qsTr("Attach file to message")
}
} }
QoAButton { QoAButton {
@@ -64,12 +58,9 @@ Rectangle {
height: 15 height: 15
width: 15 width: 15
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: attachImagesId.hovered ToolTip.text: qsTr("Attach image to message")
delay: 250
text: qsTr("Attach image to message")
}
} }
QoAButton { QoAButton {
@@ -80,12 +71,9 @@ Rectangle {
height: 15 height: 15
width: 8 width: 8
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: linkFilesId.hovered ToolTip.text: qsTr("Link file to context")
delay: 250
text: qsTr("Link file to context")
}
} }
CheckBox { CheckBox {
@@ -93,10 +81,8 @@ Rectangle {
text: qsTr("Sync open files") text: qsTr("Sync open files")
QoAToolTip { ToolTip.visible: syncOpenFilesId.hovered
visible: syncOpenFilesId.hovered ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
text: qsTr("Automatically synchronize currently opened files with the model context")
}
} }
Item { Item {
@@ -109,7 +95,7 @@ Rectangle {
visible: root.isCompressing visible: root.isCompressing
spacing: 6 spacing: 6
QoABusyIndicator { BusyIndicator {
id: compressBusyIndicator id: compressBusyIndicator
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -131,11 +117,9 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: qsTr("Cancel") text: qsTr("Cancel")
QoAToolTip { ToolTip.visible: hovered
visible: cancelCompressButtonId.hovered ToolTip.delay: 250
delay: 250 ToolTip.text: qsTr("Cancel compression")
text: qsTr("Cancel compression")
}
} }
} }
@@ -150,41 +134,20 @@ Rectangle {
height: 15 height: 15
width: 15 width: 15
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: compressButtonId.hovered ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
delay: 250
text: qsTr("Compress chat (create summarized copy using LLM)")
}
} }
QoAButton { QoAButton {
id: sendButtonId id: sendButtonId
leftPadding: root.isProcessing ? 22 : 4
icon { icon {
height: 15 height: 15
width: 15 width: 15
} }
ToolTip.visible: hovered
QoABusyIndicator { ToolTip.delay: 250
id: sendBusyIndicator
anchors.left: parent.left
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
width: 14
height: 14
running: root.isProcessing
}
QoAToolTip {
id: sendButtonTooltipId
visible: sendButtonId.hovered
delay: 250
}
} }
} }
} }

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -119,13 +118,11 @@ Rectangle {
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits) ? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
: qsTr("Reapply All (%1)").arg(root.rejectedEdits) : qsTr("Reapply All (%1)").arg(root.rejectedEdits)
QoAToolTip { ToolTip.visible: hovered
visible: applyAllButton.hovered ToolTip.delay: 250
delay: 250 ToolTip.text: root.hasPendingEdits
text: root.hasPendingEdits ? qsTr("Apply all pending and rejected edits in this message")
? qsTr("Apply all pending and rejected edits in this message") : qsTr("Reapply all rejected edits in this message")
: qsTr("Reapply all rejected edits in this message")
}
onClicked: root.applyAllClicked() onClicked: root.applyAllClicked()
} }
@@ -137,11 +134,9 @@ Rectangle {
enabled: root.hasAppliedEdits enabled: root.hasAppliedEdits
text: qsTr("Undo All (%1)").arg(root.appliedEdits) text: qsTr("Undo All (%1)").arg(root.appliedEdits)
QoAToolTip { ToolTip.visible: hovered
visible: undoAllButton.hovered ToolTip.delay: 250
delay: 250 ToolTip.text: qsTr("Undo all applied edits in this message")
text: qsTr("Undo all applied edits in this message")
}
onClicked: root.undoAllClicked() onClicked: root.undoAllClicked()
} }

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2026 Petr Mironychev // Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls

View File

@@ -1,188 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import ChatView
import UIControls
Item {
id: nav
property var chatModel
property var entries: []
property color dotColor: "#92BD6C"
property int currentMessageIndex: -1
readonly property int dotCount: entries.length
readonly property int verticalPadding: 8
readonly property int minDotSpacing: 18
readonly property real availableHeight: Math.max(0, height - 2 * verticalPadding)
readonly property real naturalHeight: dotCount > 1 ? (dotCount - 1) * minDotSpacing : 0
readonly property bool needsScrolling: naturalHeight > availableHeight
readonly property real contentHeight: needsScrolling
? naturalHeight + 2 * verticalPadding
: Math.max(height, 2 * verticalPadding)
signal messageClicked(int messageIndex)
implicitWidth: 16
function rebuild() {
entries = chatModel ? chatModel.userMessagePreviews(80) : []
Qt.callLater(scrollCurrentIntoView)
}
function updateCurrentFromModelIndex(modelIdx) {
if (modelIdx < 0) {
currentMessageIndex = -1
return
}
let best = -1
for (let i = 0; i < entries.length; ++i) {
const e = entries[i]
if (!e)
continue
const mi = e.messageIndex
if (mi <= modelIdx)
best = mi
else
break
}
currentMessageIndex = best
}
function uiIndexOf(messageIndex) {
for (let i = 0; i < entries.length; ++i) {
const e = entries[i]
if (e && e.messageIndex === messageIndex)
return i
}
return -1
}
function dotCenterY(uiIndex) {
const count = dotCount
if (count <= 1)
return contentHeight / 2
const spacing = needsScrolling
? minDotSpacing
: availableHeight / (count - 1)
return verticalPadding + spacing * uiIndex
}
function scrollCurrentIntoView() {
if (!needsScrolling || currentMessageIndex < 0)
return
const ui = uiIndexOf(currentMessageIndex)
if (ui < 0)
return
const y = dotCenterY(ui)
const margin = 24
if (y < flick.contentY + margin)
flick.contentY = Math.max(0, y - margin)
else if (y > flick.contentY + flick.height - margin)
flick.contentY = Math.min(
Math.max(0, flick.contentHeight - flick.height),
y - flick.height + margin)
}
onChatModelChanged: rebuild()
onCurrentMessageIndexChanged: scrollCurrentIntoView()
Component.onCompleted: rebuild()
Connections {
target: nav.chatModel
ignoreUnknownSignals: true
function onRowsInserted() { nav.rebuild() }
function onRowsRemoved() { nav.rebuild() }
function onModelReset() { nav.rebuild() }
function onModelReseted() { nav.rebuild() }
function onDataChanged() { nav.rebuild() }
}
Flickable {
id: flick
anchors.fill: parent
contentWidth: width
contentHeight: nav.contentHeight
interactive: nav.needsScrolling
clip: true
boundsBehavior: Flickable.StopAtBounds
Rectangle {
id: spine
visible: nav.dotCount > 1
anchors.horizontalCenter: parent.horizontalCenter
y: nav.verticalPadding
width: 1
height: Math.max(0, flick.contentHeight - 2 * nav.verticalPadding)
color: palette.mid
opacity: 0.4
}
Repeater {
model: nav.entries
delegate: Item {
id: dotItem
required property var modelData
required property int index
readonly property int msgIndex: modelData && modelData.messageIndex !== undefined
? modelData.messageIndex : -1
readonly property string preview: modelData && modelData.preview !== undefined
? modelData.preview : ""
readonly property bool isCurrent: nav.currentMessageIndex === msgIndex
width: 16
height: 14
anchors.horizontalCenter: parent.horizontalCenter
y: nav.dotCenterY(index) - height / 2
Rectangle {
id: dot
anchors.centerIn: parent
width: dotItem.isCurrent ? 11 : (dotArea.containsMouse ? 10 : 7)
height: width
radius: width / 2
color: dotArea.containsMouse
? Qt.lighter(nav.dotColor, 1.2)
: nav.dotColor
border.color: dotItem.isCurrent
? Qt.darker(nav.dotColor, 1.7)
: Qt.darker(nav.dotColor, 1.4)
border.width: dotItem.isCurrent ? 2 : 1
opacity: dotItem.isCurrent || dotArea.containsMouse ? 1.0 : 0.55
Behavior on width { NumberAnimation { duration: 120 } }
Behavior on opacity { NumberAnimation { duration: 120 } }
Behavior on color { ColorAnimation { duration: 120 } }
}
MouseArea {
id: dotArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: nav.messageClicked(dotItem.msgIndex)
QoAToolTip {
visible: dotArea.containsMouse
delay: 350
text: dotItem.preview.length > 0
? qsTr("#%1 · %2").arg(dotItem.index + 1).arg(dotItem.preview)
: qsTr("Jump to message #%1").arg(dotItem.index + 1)
}
}
}
}
}
}

View File

@@ -1,126 +0,0 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Rectangle {
id: root
// Object exposing Q_INVOKABLE QVariantList searchSkills(query).
property var skillProvider: null
property var searchResults: []
property int currentIndex: 0
signal selectionRequested()
visible: searchResults.length > 0
height: Math.min(searchResults.length * 40, 40 * 6) + 2
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
function updateSearch(query) {
searchResults = skillProvider ? skillProvider.searchSkills(query) : []
currentIndex = 0
}
function dismiss() {
searchResults = []
currentIndex = 0
}
function moveUp() {
if (currentIndex > 0)
currentIndex--
}
function moveDown() {
if (currentIndex < searchResults.length - 1)
currentIndex++
}
function currentName() {
if (currentIndex >= 0 && currentIndex < searchResults.length)
return searchResults[currentIndex].name
return ""
}
onCurrentIndexChanged: listView.positionViewAtIndex(currentIndex, ListView.Contain)
ListView {
id: listView
anchors.fill: parent
anchors.margins: 1
model: root.searchResults
currentIndex: root.currentIndex
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
id: delegateItem
required property int index
required property var modelData
width: listView.width
height: 40
color: index === root.currentIndex
? palette.highlight
: (hoverArea.containsMouse
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
: "transparent")
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.topMargin: 4
anchors.bottomMargin: 4
spacing: 1
Text {
Layout.fillWidth: true
text: "/" + delegateItem.modelData.name
color: delegateItem.index === root.currentIndex
? palette.highlightedText
: palette.text
font.bold: true
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: delegateItem.modelData.description
color: delegateItem.index === root.currentIndex
? Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.7)
: palette.mid
font.pixelSize: 11
elide: Text.ElideRight
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.currentIndex = delegateItem.index
root.selectionRequested()
}
onEntered: root.currentIndex = delegateItem.index
}
}
}
}

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@@ -11,22 +10,19 @@ import UIControls
Rectangle { Rectangle {
id: root id: root
property bool isInEditor: false
property alias saveButton: saveButtonId property alias saveButton: saveButtonId
property alias loadButton: loadButtonId property alias loadButton: loadButtonId
property alias clearButton: clearButtonId property alias clearButton: clearButtonId
property alias newChatButton: newChatButtonId
property alias tokensBadge: tokensBadgeId property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId property alias settingsButton: settingsButtonId
property alias agentSelector: agentSelectorId property alias configSelector: configSelectorId
property alias roleSelector: roleSelectorId property alias roleSelector: roleSelector
property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@@ -59,112 +55,93 @@ Rectangle {
height: 15 height: 15
width: 15 width: 15
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: pinButtonId.hovered ToolTip.text: checked ? qsTr("Unpin chat window")
delay: 250 : qsTr("Pin chat window to the top")
text: pinButtonId.checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
}
QoAButton {
id: relocateButtonId
anchors.verticalCenter: parent.verticalCenter
icon {
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
id: relocateTooltipId
visible: relocateButtonId.hovered
delay: 250
}
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
QoAToolTip {
visible: clearButtonId.hovered
delay: 250
text: qsTr("Clean chat")
}
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: newChatButtonId
visible: root.isInEditor
icon {
source: "qrc:/qt/qml/ChatView/icons/new-chat-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: newChatButtonId.hovered
delay: 250
text: qsTr("Open new chat in a new tab")
}
} }
QoAComboBox { QoAComboBox {
id: agentSelectorId id: configSelectorId
implicitHeight: 25 implicitHeight: 25
model: [] model: []
currentIndex: 0 currentIndex: 0
QoAToolTip { ToolTip.visible: hovered
visible: agentSelectorId.hovered ToolTip.delay: 250
delay: 250 ToolTip.text: qsTr("Switch saved AI configuration")
text: qsTr("Select chat agent (provider and model come from the agent)")
}
} }
QoAComboBox { QoAComboBox {
id: roleSelectorId id: roleSelector
implicitHeight: 25 implicitHeight: 25
model: [] model: []
currentIndex: 0 currentIndex: 0
QoAToolTip { ToolTip.visible: hovered
visible: roleSelectorId.hovered ToolTip.delay: 250
delay: 250 ToolTip.text: qsTr("Switch agent role (different system prompts)")
text: qsTr("Select the role (system prompt) for the chat")
}
} }
} }
Row { Row {
spacing: 10 spacing: 10
QoAButton {
id: toolsButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
QoAButton { QoAButton {
id: settingsButtonId id: settingsButtonId
@@ -177,11 +154,9 @@ Rectangle {
width: 15 width: 15
} }
QoAToolTip { ToolTip.visible: hovered
visible: settingsButtonId.hovered ToolTip.delay: 250
delay: 250 ToolTip.text: qsTr("Open Chat Assistant Settings")
text: qsTr("Open Chat Assistant Settings")
}
} }
QoASeparator { QoASeparator {
@@ -207,11 +182,9 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
QoAToolTip { ToolTip.visible: containsMouse
visible: parent.containsMouse && recentPathId.text.length > 0 ToolTip.delay: 500
text: recentPathId.text ToolTip.text: recentPathId.text
delay: 500
}
} }
} }
} }
@@ -232,12 +205,9 @@ Rectangle {
height: 15 height: 15
width: 8 width: 8
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: saveButtonId.hovered ToolTip.text: qsTr("Save chat to *.json file")
delay: 250
text: qsTr("Save chat to *.json file")
}
} }
QoAButton { QoAButton {
@@ -248,12 +218,9 @@ Rectangle {
height: 15 height: 15
width: 8 width: 8
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: loadButtonId.hovered ToolTip.text: qsTr("Load chat from *.json file")
delay: 250
text: qsTr("Load chat from *.json file")
}
} }
QoAButton { QoAButton {
@@ -264,12 +231,9 @@ Rectangle {
height: 15 height: 15
width: 15 width: 15
} }
ToolTip.visible: hovered
QoAToolTip { ToolTip.delay: 250
visible: openChatHistoryId.hovered ToolTip.text: qsTr("Show in system")
delay: 250
text: qsTr("Show in system")
}
} }
QoASeparator {} QoASeparator {}
@@ -284,11 +248,9 @@ Rectangle {
width: 15 width: 15
} }
QoAToolTip { ToolTip.visible: hovered
visible: contextButtonId.hovered ToolTip.delay: 250
delay: 250 ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
text: qsTr("View chat context (system prompt, role, rules)")
}
} }
Badge { Badge {
@@ -298,6 +260,21 @@ Rectangle {
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold") ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
} }
QoASeparator {}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
} }
} }
} }

View File

@@ -1,7 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt> // Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

235
ConfigurationManager.cpp Normal file
View File

@@ -0,0 +1,235 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ConfigurationManager.hpp"
#include <settings/ButtonAspect.hpp>
#include <QTimer>
#include "QodeAssisttr.h"
namespace QodeAssist {
ConfigurationManager &ConfigurationManager::instance()
{
static ConfigurationManager instance;
return instance;
}
void ConfigurationManager::init()
{
setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
m_generalSettings.qrTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
updateTemplateDescription(m_generalSettings.qrTemplate);
}
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
}
ConfigurationManager::ConfigurationManager(QObject *parent)
: QObject(parent)
, m_generalSettings(Settings::generalSettings())
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
{}
void ConfigurationManager::setupConnections()
{
using Config = ConfigurationManager;
using Button = ButtonAspect;
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.qrTemplate);
});
}
void ConfigurationManager::selectProvider()
{
const auto providersList = m_providersManager.providersNames();
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider
: settingsButton == &m_generalSettings.qrSelectProvider
? m_generalSettings.qrProvider
: m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog(
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
});
}
void ConfigurationManager::selectModel()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
: m_generalSettings.caUrl.volatileValue();
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model
: isQuickRefactor ? m_generalSettings.qrModel
: m_generalSettings.caModel);
if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
return;
}
provider->getInstalledModels(providerUrl)
.then(this, [this, targetSettings](const QList<QString> &modelList) {
if (modelList.isEmpty()) {
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
return;
}
m_generalSettings.showSelectionDialog(
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
});
}
}
void ConfigurationManager::selectTemplate()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1
? m_templateManger.getFimTemplatesForProvider(providerID)
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: isQuickRefactor ? m_generalSettings.qrTemplate
: m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog(
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
});
}
void ConfigurationManager::selectUrl()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
QStringList urls;
for (const auto &name : m_providersManager.providersNames()) {
const auto url = m_providersManager.getProviderByName(name)->url();
if (!urls.contains(url))
urls.append(url);
}
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
: settingsButton == &m_generalSettings.ccPreset1SetUrl
? m_generalSettings.ccPreset1Url
: settingsButton == &m_generalSettings.qrSetUrl
? m_generalSettings.qrUrl
: m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
});
}
} // namespace QodeAssist

47
ConfigurationManager.hpp Normal file
View File

@@ -0,0 +1,47 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include "pluginllmcore/PromptTemplateManager.hpp"
#include "pluginllmcore/ProvidersManager.hpp"
#include "settings/GeneralSettings.hpp"
namespace QodeAssist {
class ConfigurationManager : public QObject
{
Q_OBJECT
public:
static ConfigurationManager &instance();
void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots:
void selectProvider();
void selectModel();
void selectTemplate();
void selectUrl();
private:
explicit ConfigurationManager(QObject *parent = nullptr);
~ConfigurationManager() = default;
ConfigurationManager(const ConfigurationManager &) = delete;
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::ProvidersManager &m_providersManager;
PluginLLMCore::PromptTemplateManager &m_templateManger;
void setupConnections();
};
} // namespace QodeAssist

21
LICENSE
View File

@@ -1,24 +1,3 @@
===============================================================
ADDITIONAL TERMS UNDER GPLv3 SECTION 7(b)
===============================================================
In accordance with Section 7(b) of the GNU General Public License v3.0,
the following additional attribution term applies to QodeAssist:
You must preserve all author attributions, copyright notices, and the
project name "QodeAssist" in all copies and modified versions,
including in source file headers, the plugin metadata
(QodeAssist.json.in), and the About dialog or equivalent user-facing
identification. Modified versions must be clearly marked as different
from the original.
This is a reasonable attribution requirement permitted under GPLv3
§7(b) and §7(c). It supplements the notice-preservation obligations of
§4 and §5.
Copyright (C) 2024-2026 Petr Mironychev
===============================================================
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
@@ -9,47 +8,27 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <Agent.hpp>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <PluginBlocks.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "sources/common/ContextData.hpp"
#include <LLMQore/ContentBlocks.hpp>
#include <memory>
#include <vector>
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp" #include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp" #include "context/Utils.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "sources/settings/PipelinesConfig.hpp" #include <pluginllmcore/RulesLoader.hpp>
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
AgentFactory &agentFactory, PluginLLMCore::IProviderRegistry &providerRegistry,
SessionManager &sessionManager, PluginLLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger) IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings) : m_generalSettings(generalSettings)
, m_completeSettings(completeSettings) , m_completeSettings(completeSettings)
, m_agentFactory(agentFactory) , m_providerRegistry(providerRegistry)
, m_sessionManager(sessionManager) , m_promptProvider(promptProvider)
, m_documentReader(documentReader) , m_documentReader(documentReader)
, m_performanceLogger(performanceLogger) , m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
@@ -71,34 +50,33 @@ void LLMClientInterface::startImpl()
emit started(); emit started();
} }
void LLMClientInterface::onCompletionFinished(const QString &requestId) void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
QString fullText; const RequestContext &ctx = it.value();
if (Session *session = it.value().session) { sendCompletionToClient(fullText, ctx.originalRequest, true);
if (auto *history = session->history(); history && !history->isEmpty())
fullText = history->messages().back().text();
}
const QJsonObject originalRequest = it.value().originalRequest;
sendCompletionToClient(fullText, originalRequest, true); m_activeRequests.erase(it);
finishRequest(requestId); m_performanceLogger.endTimeMeasurement(requestId);
} }
void LLMClientInterface::onCompletionFailed(const QString &requestId, const QString &error) void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; return;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response; QJsonObject response;
response["jsonrpc"] = "2.0"; response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = it.value().originalRequest["id"]; response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
QJsonObject errorObject; QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code errorObject["code"] = -32603; // Internal error code
@@ -106,21 +84,9 @@ void LLMClientInterface::onCompletionFailed(const QString &requestId, const QStr
response["error"] = errorObject; response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
finishRequest(requestId);
}
void LLMClientInterface::finishRequest(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
m_activeRequests.erase(it); m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId); m_performanceLogger.endTimeMeasurement(requestId);
if (session)
m_sessionManager.release(session);
} }
void LLMClientInterface::sendData(const QByteArray &data) void LLMClientInterface::sendData(const QByteArray &data)
@@ -153,15 +119,26 @@ void LLMClientInterface::sendData(const QByteArray &data)
void LLMClientInterface::handleCancelRequest() void LLMClientInterface::handleCancelRequest()
{ {
const auto requests = m_activeRequests; QSet<PluginLLMCore::Provider *> providers;
m_activeRequests.clear(); for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
for (auto it = requests.begin(); it != requests.end(); ++it) { providers.insert(it.value().provider);
m_performanceLogger.endTimeMeasurement(it.key()); }
if (Session *session = it.value().session)
m_sessionManager.release(session);
} }
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear();
LOG_MESSAGE("All requests cancelled and state cleared"); LOG_MESSAGE("All requests cancelled and state cleared");
} }
@@ -244,87 +221,124 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return; return;
} }
const QString agentName = pickCompletionAgent(filePath); auto updatedContext = prepareContext(request, documentInfo);
if (agentName.isEmpty()) {
QString error = QString("No code completion agent matches: %1").arg(filePath); bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
QString sessionError; auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
Session *session = m_sessionManager.acquire(agentName, &sessionError); : m_generalSettings.ccPreset1Template();
if (!session) {
LOG_MESSAGE(sessionError);
sendErrorResponse(request, sessionError);
return;
}
Templates::ContextData context = prepareContext(request, documentInfo); auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QString editorContext; if (!promptTemplate) {
if (context.fileContext.has_value()) QString error = QString("No template found with name: %1").arg(templateName);
editorContext.append(context.fileContext.value());
if (m_completeSettings.useOpenFilesContext())
editorContext.append(m_contextManager->openedFilesContext({filePath}));
if (!editorContext.isEmpty())
session->systemPrompt()->setLayer(QStringLiteral("completion.context"), editorContext);
connect(session, &Session::finished, this, [this, session](const LLMQore::RequestID &, const QString &) {
onCompletionFinished(requestIdForSession(session));
});
connect(session, &Session::failed, this, [this, session](const LLMQore::RequestID &, const QodeAssist::ErrorInfo &error) {
onCompletionFailed(requestIdForSession(session), error.message);
});
if (auto *client = session->client())
client->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
blocks.push_back(std::make_unique<CompletionContent>(
context.prefix.value_or(QString()), context.suffix.value_or(QString())));
const LLMQore::RequestID requestId = session->send(std::move(blocks), /*toolsOverride=*/false);
if (requestId.isEmpty()) {
QString error = QString("Failed to start completion request for agent '%1': %2")
.arg(agentName, session->lastError().message);
session->deleteLater();
LOG_MESSAGE(error); LOG_MESSAGE(error);
sendErrorResponse(request, error); sendErrorResponse(request, error);
return; return;
} }
m_activeRequests[requestId] = {request, session}; QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for completion");
}
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<PluginLLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
PluginLLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_performanceLogger.startTimeMeasurement(requestId); m_performanceLogger.startTimeMeasurement(requestId);
} }
QString LLMClientInterface::pickCompletionAgent(const QString &filePath) const PluginLLMCore::ContextData LLMClientInterface::prepareContext(
{
const QStringList roster = Settings::PipelinesConfig::load().rosters.codeCompletion;
if (roster.isEmpty())
return {};
AgentRouter::Context ctx;
ctx.filePath = filePath;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
ctx.projectName = project->displayName();
return AgentRouter::pickAgent(roster, ctx, m_agentFactory);
}
QString LLMClientInterface::requestIdForSession(Session *session) const
{
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
if (it.value().session == session)
return it.key();
}
return {};
}
Templates::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo) const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
@@ -338,6 +352,14 @@ Templates::ContextData LLMClientInterface::prepareContext(
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings); return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
} }
QString LLMClientInterface::resolveEndpoint(
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
{
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
}
Context::ContextManager *LLMClientInterface::contextManager() const Context::ContextManager *LLMClientInterface::contextManager() const
{ {
return m_contextManager; return m_contextManager;
@@ -346,6 +368,15 @@ Context::ContextManager *LLMClientInterface::contextManager() const
void LLMClientInterface::sendCompletionToClient( void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete) const QString &completion, const QJsonObject &request, bool isComplete)
{ {
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response; QJsonObject response;

View File

@@ -1,18 +1,17 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <LLMQore/BaseClient.hpp>
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <QPointer>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp> #include <context/ProgrammingLanguage.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp> #include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
@@ -22,14 +21,6 @@ class QNetworkAccessManager;
namespace QodeAssist { namespace QodeAssist {
class AgentFactory;
class Session;
class SessionManager;
namespace Templates {
struct ContextData;
}
class LLMClientInterface : public LanguageClient::BaseClientInterface class LLMClientInterface : public LanguageClient::BaseClientInterface
{ {
Q_OBJECT Q_OBJECT
@@ -38,8 +29,8 @@ public:
LLMClientInterface( LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
AgentFactory &agentFactory, PluginLLMCore::IProviderRegistry &providerRegistry,
SessionManager &sessionManager, PluginLLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger); IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override; ~LLMClientInterface() override;
@@ -59,6 +50,10 @@ public:
protected: protected:
void startImpl() override; void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
private: private:
void handleInitialize(const QJsonObject &request); void handleInitialize(const QJsonObject &request);
void handleShutdown(const QJsonObject &request); void handleShutdown(const QJsonObject &request);
@@ -68,26 +63,22 @@ private:
void handleCancelRequest(); void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage); void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
void onCompletionFinished(const QString &requestId);
void onCompletionFailed(const QString &requestId, const QString &error);
void finishRequest(const QString &requestId);
QString requestIdForSession(Session *session) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
QPointer<Session> session; PluginLLMCore::Provider *provider;
}; };
Templates::ContextData prepareContext( PluginLLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString pickCompletionAgent(const QString &filePath) const; QString resolveEndpoint(
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings; const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings; const Settings::GeneralSettings &m_generalSettings;
AgentFactory &m_agentFactory; PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
SessionManager &m_sessionManager; PluginLLMCore::IProviderRegistry &m_providerRegistry;
Context::IDocumentReader &m_documentReader; Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger; IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;

View File

@@ -1,7 +1,6 @@
// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>

View File

@@ -20,8 +20,6 @@
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/ */
#pragma once #pragma once

View File

@@ -20,8 +20,6 @@
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/ */
#pragma once #pragma once

View File

@@ -1,12 +1,12 @@
{ {
"Id" : "qodeassist", "Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.9.20", "Version" : "0.9.12",
"CompatVersion" : "${IDE_VERSION}", "CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev", "Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev", "VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GPLv3 with additional attribution terms (§7b) — see LICENSE", "License" : "GPLv3",
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).", "Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
"Url" : "https://github.com/Palm1r/QodeAssist", "Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist", "DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",

View File

@@ -20,8 +20,6 @@
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
*/ */
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
@@ -159,16 +157,6 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_refactorWidgetHandler = new RefactorWidgetHandler(this); m_refactorWidgetHandler = new RefactorWidgetHandler(this);
} }
void QodeAssistClient::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QodeAssistClient::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
QodeAssistClient::~QodeAssistClient() QodeAssistClient::~QodeAssistClient()
{ {
cleanupConnections(); cleanupConnections();
@@ -273,8 +261,9 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
return; return;
if (m_llmClient->contextManager()->shouldIgnore( if (m_llmClient->contextManager()
editor->textDocument()->filePath().toUrlishString())) { ->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -318,8 +307,9 @@ void QodeAssistClient::requestQuickRefactor(
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
if (m_llmClient->contextManager()->shouldIgnore( if (m_llmClient->contextManager()
editor->textDocument()->filePath().toUrlishString())) { ->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString())); .arg(editor->textDocument()->filePath().toUrlishString()));
return; return;
@@ -327,8 +317,6 @@ void QodeAssistClient::requestQuickRefactor(
if (!m_refactorHandler) { if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this); m_refactorHandler = new QuickRefactorHandler(this);
m_refactorHandler->setSessionManager(m_sessionManager);
m_refactorHandler->setAgentFactory(m_agentFactory);
connect( connect(
m_refactorHandler, m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted, &QuickRefactorHandler::refactoringCompleted,

View File

@@ -1,12 +1,10 @@
// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QObject> #include <QObject>
#include <QPointer>
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp" #include "LSPCompletion.hpp"
@@ -17,12 +15,11 @@
#include "widgets/EditorChatButtonHandler.hpp" #include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp" #include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class AgentFactory;
class QodeAssistClient : public LanguageClient::Client class QodeAssistClient : public LanguageClient::Client
{ {
Q_OBJECT Q_OBJECT
@@ -30,9 +27,6 @@ public:
explicit QodeAssistClient(LLMClientInterface *clientInterface); explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override; ~QodeAssistClient() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void openDocument(TextEditor::TextDocument *document) override; void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override; bool canOpenProject(ProjectExplorer::Project *project) override;
@@ -73,8 +67,6 @@ private:
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr}; RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient; LLMClientInterface *m_llmClient;
SessionManager *m_sessionManager{nullptr};
AgentFactory *m_agentFactory{nullptr};
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -11,16 +10,4 @@ const char MENU_ID[] = "QodeAssist.Menu";
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion"; const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
const char QODE_ASSIST_CHAT_CONTEXT[] = "QodeAssist.ChatContext";
const char QODE_ASSIST_CHAT_NAV_ID[] = "QodeAssistChat";
const char QODE_ASSIST_CHAT_EDITOR_ID[] = "QodeAssist.ChatEditor";
const char QODE_ASSIST_SHOW_CHAT_ACTION[] = "QodeAssist.ShowChatView";
const char QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION[] = "QodeAssist.OpenChatWindow";
const char QODE_ASSIST_NEW_CHAT_ACTION[] = "QodeAssist.NewChat";
const char QODE_ASSIST_CHAT_SEND_MESSAGE[] = "QodeAssist.Chat.SendMessage";
const char QODE_ASSIST_CHAT_CLEAR_SESSION[] = "QodeAssist.Chat.ClearSession";
const char QODE_ASSIST_CHAT_SHOW_IN_RIGHT[] = "QodeAssist.Chat.ShowInRightSidebar";
} // namespace QodeAssist::Constants } // namespace QodeAssist::Constants

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,43 +1,26 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <context/DocumentContextReader.hpp> #include <context/DocumentContextReader.hpp>
#include <pluginllmcore/ResponseCleaner.hpp>
#include <context/DocumentReaderQtCreator.hpp> #include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp> #include <context/Utils.hpp>
#include <pluginllmcore/PromptTemplateManager.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <pluginllmcore/RulesLoader.hpp>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include <sources/common/ResponseCleaner.hpp> #include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp> #include <settings/QuickRefactorSettings.hpp>
#include <settings/ToolsSettings.hpp> #include <settings/ToolsSettings.hpp>
#include "sources/common/ContextData.hpp"
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "sources/settings/PipelinesConfig.hpp"
#include "tools/ToolsRegistration.hpp"
namespace QodeAssist { namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent) QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
@@ -50,16 +33,6 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
QuickRefactorHandler::~QuickRefactorHandler() {} QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
void QuickRefactorHandler::sendRefactorRequest( void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions) TextEditor::TextEditorWidget *editor, const QString &instructions)
{ {
@@ -114,109 +87,95 @@ void QuickRefactorHandler::sendRefactorRequest(
prepareAndSendRequest(editor, instructions, range); prepareAndSendRequest(editor, instructions, range);
} }
QString QuickRefactorHandler::pickRefactorAgent(const QString &filePath) const
{
const QStringList roster = Settings::PipelinesConfig::load().rosters.quickRefactor;
if (roster.isEmpty() || !m_agentFactory)
return {};
AgentRouter::Context ctx;
ctx.filePath = filePath;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
ctx.projectName = project->displayName();
return AgentRouter::pickAgent(roster, ctx, *m_agentFactory);
}
void QuickRefactorHandler::prepareAndSendRequest( void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range) const Utils::Text::Range &range)
{ {
const auto emitError = [this, editor](const QString &error) { auto &settings = Settings::generalSettings();
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
const auto providerName = settings.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
RefactorResult result; RefactorResult result;
result.success = false; result.success = false;
result.errorMessage = error; result.errorMessage = error;
result.editor = editor; result.editor = editor;
emit refactoringCompleted(result); emit refactoringCompleted(result);
};
if (!m_sessionManager) {
emitError(QStringLiteral("Quick refactor session manager is not available"));
return; return;
} }
const QString filePath = editor->textDocument()->filePath().toUrlishString(); const auto templateName = settings.qrTemplate();
const QString agentName = pickRefactorAgent(filePath); auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (agentName.isEmpty()) {
emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath)); if (!promptTemplate) {
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return; return;
} }
QString sessionError; QJsonObject payload{
Session *session = m_sessionManager->acquire(agentName, &sessionError); {"model", Settings::generalSettings().qrModel()}, {"stream", true}};
if (!session) {
emitError(sessionError.isEmpty() ? QStringLiteral("No quick refactor agent selected")
: sessionError);
return;
}
auto *client = session->client(); PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
if (!client) {
m_sessionManager->removeSession(session);
emitError(QStringLiteral("Quick refactor agent has no live client"));
return;
}
const bool enableTools = Settings::quickRefactorSettings().useTools(); bool enableTools = Settings::quickRefactorSettings().useTools();
if (enableTools) { bool enableThinking = Settings::quickRefactorSettings().useThinking();
m_sessionManager->toolContributors().contribute(client->tools()); provider->prepareRequest(
client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations()); payload,
} promptTemplate,
context,
PluginLLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
session->systemPrompt()->setLayer( provider->client()->setMaxToolContinuations(
QStringLiteral("refactor"), buildSystemPrompt(editor, range)); Settings::toolsSettings().maxToolContinuations());
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_isRefactoringInProgress = true; m_isRefactoringInProgress = true;
connect( connect(
session, &Session::finished, this, provider->client(),
[this](const LLMQore::RequestID &id, const QString &) { onRefactorFinished(id); }); &::LLMQore::BaseClient::requestCompleted,
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
connect( connect(
session, &Session::failed, this, provider->client(),
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) { &::LLMQore::BaseClient::requestFailed,
onRefactorFailed(id, error); this,
}); &QuickRefactorHandler::handleRequestFailed,
Qt::UniqueConnection);
std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
const QString userMessage = instructions.isEmpty()
? QStringLiteral("Refactor the code to improve its quality and maintainability.")
: instructions;
blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
const LLMQore::RequestID requestId = session->send(std::move(blocks), enableTools);
if (requestId.isEmpty()) {
m_isRefactoringInProgress = false;
const QString reason = session->lastError().message;
m_sessionManager->removeSession(session);
emitError(QStringLiteral("Failed to start quick refactor request for agent '%1': %2")
.arg(agentName, reason));
return;
}
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
m_lastRequestId = requestId; m_lastRequestId = requestId;
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session}; QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider};
} }
QString QuickRefactorHandler::buildSystemPrompt( PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range) TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
{ {
Q_UNUSED(range) PluginLLMCore::ContextData context;
auto textDocument = editor->textDocument(); auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader; Context::DocumentReaderQtCreator documentReader;
@@ -224,7 +183,7 @@ QString QuickRefactorHandler::buildSystemPrompt(
if (!documentInfo.document) { if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available"); LOG_MESSAGE("Error: Document is not available");
return Settings::quickRefactorSettings().systemPrompt(); return context;
} }
QTextCursor cursor = editor->textCursor(); QTextCursor cursor = editor->textCursor();
@@ -300,6 +259,17 @@ QString QuickRefactorHandler::buildSystemPrompt(
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt(); QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
project, PluginLLMCore::RulesContext::QuickRefactor);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for quick refactor");
}
}
systemPrompt += "\n\nFile information:"; systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType; systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath; systemPrompt += "\nFile path: " + documentInfo.filePath;
@@ -368,92 +338,87 @@ QString QuickRefactorHandler::buildSystemPrompt(
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath}); systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
} }
return systemPrompt; context.systemPrompt = systemPrompt;
QVector<PluginLLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
m_isRefactoringInProgress = false;
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
result.editor = m_currentEditor;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
} }
void QuickRefactorHandler::cancelRequest() void QuickRefactorHandler::cancelRequest()
{ {
if (!m_isRefactoringInProgress) if (m_isRefactoringInProgress) {
return; auto id = m_lastRequestId;
const auto id = m_lastRequestId; for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
m_isRefactoringInProgress = false; if (it.key() == id) {
m_lastRequestId.clear(); const RequestContext &ctx = it.value();
ctx.provider->cancelRequest(id);
m_activeRequests.erase(it);
break;
}
}
auto it = m_activeRequests.find(id); m_isRefactoringInProgress = false;
if (it != m_activeRequests.end()) {
Session *session = it.value().session; RefactorResult result;
m_activeRequests.erase(it); result.success = false;
if (session && m_sessionManager) result.errorMessage = "Refactoring request was cancelled";
m_sessionManager->release(session); emit refactoringCompleted(result);
} }
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
} }
void QuickRefactorHandler::onRefactorFinished(const QString &requestId) void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
{ {
if (requestId != m_lastRequestId) if (requestId == m_lastRequestId) {
return; m_activeRequests.remove(requestId);
QJsonObject request{{"id", requestId}};
auto it = m_activeRequests.find(requestId); handleLLMResponse(fullText, request, true);
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
if (it != m_activeRequests.end())
m_activeRequests.erase(it);
QString fullText;
if (session) {
if (auto *history = session->history(); history && !history->isEmpty())
fullText = history->messages().back().text();
} }
m_isRefactoringInProgress = false;
m_lastRequestId.clear();
const QString cleanedResponse = ResponseCleaner::clean(fullText);
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
result.editor = m_currentEditor;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
if (session && m_sessionManager)
m_sessionManager->release(session);
} }
void QuickRefactorHandler::onRefactorFailed( void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
const QString &requestId, const QodeAssist::ErrorInfo &error)
{ {
if (requestId != m_lastRequestId) if (requestId == m_lastRequestId) {
return; m_activeRequests.remove(requestId);
m_isRefactoringInProgress = false;
auto it = m_activeRequests.find(requestId); RefactorResult result;
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr; result.success = false;
if (it != m_activeRequests.end()) result.errorMessage = error;
m_activeRequests.erase(it); result.editor = m_currentEditor;
emit refactoringCompleted(result);
m_isRefactoringInProgress = false; }
m_lastRequestId.clear();
RefactorResult result;
result.success = false;
result.errorMessage = error.message;
result.editor = m_currentEditor;
emit refactoringCompleted(result);
if (session && m_sessionManager)
m_sessionManager->release(session);
} }
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,27 +1,21 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QJsonObject> #include <QJsonObject>
#include <QObject> #include <QObject>
#include <QPointer>
#include <LLMQore/BaseClient.hpp>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <utils/textutils.h> #include <utils/textutils.h>
#include <ErrorInfo.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist { namespace QodeAssist {
class SessionManager;
class Session;
class AgentFactory;
struct RefactorResult struct RefactorResult
{ {
QString newText; QString newText;
@@ -39,9 +33,6 @@ public:
explicit QuickRefactorHandler(QObject *parent = nullptr); explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override; ~QuickRefactorHandler() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions); void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest(); void cancelRequest();
@@ -50,26 +41,28 @@ public:
signals: signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result); void refactoringCompleted(const QodeAssist::RefactorResult &result);
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
private: private:
void prepareAndSendRequest( void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range); const Utils::Text::Range &range);
void onRefactorFinished(const QString &requestId); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error); PluginLLMCore::ContextData prepareContext(
QString buildSystemPrompt( TextEditor::TextEditorWidget *editor,
TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range); const Utils::Text::Range &range,
QString pickRefactorAgent(const QString &filePath) const; const QString &instructions);
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
QPointer<Session> session; PluginLLMCore::Provider *provider;
}; };
QPointer<SessionManager> m_sessionManager;
QPointer<AgentFactory> m_agentFactory;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor; TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange; Utils::Text::Range m_currentRange;

127
README.md
View File

@@ -6,7 +6,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Discord](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf) [![Discord](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral, Qwen, DeepSeek), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet). ![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
⚠️ **Important Notice About Paid Providers** ⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services: > When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
@@ -35,12 +35,10 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking - **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library - **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs - **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
- **Agent Skills** — reusable folders of specialized instructions loaded on demand; discovered from `.qodeassist/skills/` and `.claude/skills/`, invoked automatically, with `/skill`, or always-on
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge) - **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet) - **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
- **File Context** — attach, link, or auto-sync open editor files for richer prompts - **File Context** — attach, link, or auto-sync open editor files for richer prompts
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint - **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, any OpenAI-compatible endpoint
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control - **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users! **Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
@@ -55,11 +53,6 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details> </details>
<details>
<summary>Chat View Mode: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/5914dd78-c8a4-4d35-889a-10ec493d4c4b" width="600" alt="QodeAssistChat2">
</details>
<details> <details>
<summary>Multiline Code completion: (click to expand)</summary> <summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@@ -92,27 +85,7 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
## Install plugin to QtCreator ## Install plugin to QtCreator
### Method 1: Using the Extension Registry (Recommended) ### Method 1: Using QodeAssistUpdater (Beta)
You can install and update QodeAssist directly from within Qt Creator by adding the QodeAssist registry as an external extension repository.
1. Open the Extensions page (`Qt Creator → Extensions`) and switch to the **Browser** tab
2. Enable **Use External Repository**
3. Next to **Repository URLs**, click **Add** and paste the registry archive URL matching your Qt Creator version:
- **Latest (QtC 19)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
- **QtC 19**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc19.tar.gz`
- **QtC 18**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc18.tar.gz`
<details>
<summary>Example of extension registry: (click to expand)</summary>
<img width="600" alt="RegistryExample" src="https://github.com/user-attachments/assets/8ab8cf10-72e7-4961-8c5a-21d530378a05">
</details>
4. Click **Apply** — QodeAssist will appear in the extensions list, where you can **Install** it
5. Updates can be installed from the same screen when a new version is published
> **Note:** This is an external repository not maintained by The Qt Company. By adding it you accept responsibility for managing the associated risks, as stated in the Extensions page.
### Method 2: Using QodeAssistUpdater (Beta)
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification. QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
@@ -140,7 +113,7 @@ Download pre-built binary from [QodeAssistUpdater releases](https://github.com/P
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater). For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
### Method 3: Manual Installation ### Method 2: Manual Installation
1. Install Latest Qt Creator 1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator 2. Download the QodeAssist plugin for your Qt Creator
@@ -170,8 +143,6 @@ The Quick Setup feature provides one-click configuration for popular cloud AI mo
- **OpenAI** (gpt-5.2-codex) - **OpenAI** (gpt-5.2-codex)
- **Mistral AI** (Codestral 2501) - **Mistral AI** (Codestral 2501)
- **Google AI** (Gemini 2.5 Flash) - **Google AI** (Gemini 2.5 Flash)
- **Qwen** (Qwen3.6 Plus, Qwen3.7 Max)
- **DeepSeek** (DeepSeek V4 Flash, DeepSeek V4 Pro)
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings 3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go! All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
@@ -192,8 +163,6 @@ For advanced users or local models, choose your preferred provider and follow th
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API - **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral** - **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
- **[Google AI](docs/google-ai-configuration.md)** — Gemini - **[Google AI](docs/google-ai-configuration.md)** — Gemini
- **Qwen (Alibaba)** — DashScope OpenAI-compatible Chat and Responses endpoints
- **DeepSeek** — `deepseek-chat` and `deepseek-reasoner` (reasoning shown as thinking)
- **OpenAI-compatible** — OpenRouter and any custom endpoint - **OpenAI-compatible** — OpenRouter and any custom endpoint
### Recommended Models for Best Experience ### Recommended Models for Best Experience
@@ -216,9 +185,9 @@ For optimal coding assistance, we recommend using these top-tier models:
### Additional Configuration ### Additional Configuration
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents or override bundled ones with TOML profiles
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts - **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens - **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore` - **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
## Features ## Features
@@ -258,7 +227,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries - **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
- **[File Context](docs/file-context.md)** - Attach or link files for better context - **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional) - Automatic syncing with open editor files (optional)
- Extended thinking / reasoning mode - shows streamed chain-of-thought for reasoning models (Claude, Google, OpenAI Responses, and OpenAI-compatible endpoints returning `reasoning_content` such as DeepSeek, Qwen, LM Studio, OpenRouter) - 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
@@ -284,41 +253,6 @@ Chat and Quick Refactor can call tools to inspect and modify your project. Each
| `execute_terminal_command` | Run a shell command (with confirmation) | | `execute_terminal_command` | Run a shell command (with confirmation) |
| `todo_tool` | Track multi-step task progress during a conversation | | `todo_tool` | Track multi-step task progress during a conversation |
### Skills
**Agent Skills** package specialized instructions and workflows into reusable folders the AI loads on demand. QodeAssist implements the open [Agent Skills](https://agentskills.io) format, so skills authored for Claude Code, Cursor, or other agents work as-is.
A skill is a folder containing a `SKILL.md` file — YAML frontmatter (`name`, `description`) plus Markdown instructions:
```
my-skill/
└── SKILL.md
```
```markdown
---
name: my-skill
description: What the skill does and when to use it.
---
# My Skill
Step-by-step instructions for the task...
```
**Where skills are discovered:**
- **Project skills** — project-relative subdirectories (default `.qodeassist/skills/` and `.claude/skills/`), configured in `Projects → QodeAssist → Skills`. Project skills win over global ones on a name collision.
- **Global skills** — absolute directories shared across all projects (default includes `~/.claude/skills/`), configured in `Tools → Options → QodeAssist → Skills`.
Both settings pages show the list of currently discovered skills.
**How skills are used in Chat:**
- **Automatically** — each skill's name and description is added to the system prompt; when a request matches, the model loads the full instructions via the `load_skill` tool (requires a tool-calling model).
- **Explicitly** — type `/` in the chat input and pick a skill from the popup; its instructions are injected into that one message. Works with any model.
- **Always-on** — a skill whose frontmatter has `metadata: always-on: "true"` is injected into every chat request automatically.
Enable or disable the whole feature in `Tools → Options → QodeAssist → Skills`.
### MCP Server ### MCP Server
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client. QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
@@ -473,7 +407,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
- **Custom Instructions** provide reusable templates that can be augmented with specific details - **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled - **Tool Calling** is available for Chat and Quick Refactor when enabled
See [Agent Roles Guide](docs/agent-roles.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
@@ -520,7 +454,6 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
- [x] Quick refactoring with custom-instructions library - [x] Quick refactoring with custom-instructions library
- [x] Diff sharing with models - [x] Diff sharing with models
- [x] Tools / function calling (file I/O, build, terminal, diagnostics) - [x] Tools / function calling (file I/O, build, terminal, diagnostics)
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
- [x] Project-specific rules (`.qodeassist/rules/`) - [x] Project-specific rules (`.qodeassist/rules/`)
- [x] MCP (Model Context Protocol) — QodeAssist as a server - [x] MCP (Model Context Protocol) — QodeAssist as a server
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported) - [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
@@ -532,12 +465,11 @@ If you find QodeAssist helpful, there are several ways you can support the proje
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository. 1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist). 2. **Contribute**: Feel free to submit pull requests with bug fixes or new features.
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers. 3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following: 4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
- Paypal: [my paypalme page](https://www.paypal.com/paypalme/palm1r)
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t` - Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D` - Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy` - Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
@@ -580,10 +512,6 @@ cmake --build .
## For Contributors ## For Contributors
### Adding an agent preset
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
### Code Style ### Code Style
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc - **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
@@ -594,45 +522,6 @@ New provider/model presets are plain TOML — extend a provider base, register t
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc). For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
## License
QodeAssist is licensed under the **GNU General Public License v3.0**
(see [`LICENSE`](LICENSE)), with **additional attribution terms under
GPLv3 Section 7(b)**.
You are free to use, modify, and redistribute QodeAssist under GPL-3.0,
but you **must preserve** the original author attribution, copyright
notices, and project identification — including in source file headers,
the plugin metadata (`QodeAssist.json.in`), and the About dialog or
equivalent user-facing identification. Modified versions must be clearly
marked as different from the original.
### Commercial licensing
QodeAssist is also available under a separate commercial license for use
in proprietary or closed-source products without GPL-3.0 obligations.
For commercial licensing inquiries, contact **palm1r-github-dev@pm.me**.
### Qt Creator components and attributions
QodeAssist is a plugin for Qt Creator and incorporates certain components
(plugin templates, API headers, and related boilerplate) originating from
Qt Creator, which are copyright (C) The Qt Company Ltd.
These components are provided by The Qt Company under the GNU General
Public License version 3, annotated with **The Qt Company GPL Exception
1.0**. This exception permits the development and distribution of Qt
Creator plugins under licenses of the plugin author's own choosing,
notwithstanding the GPL's general linking requirements. It is this
exception that allows QodeAssist to be offered under both GPL-3.0 and a
separate commercial license.
The original copyright and license notices of The Qt Company are
preserved in the relevant source files and must not be removed.
For Qt Creator's licensing terms, see
[LICENSE.GPL3-EXCEPT](https://github.com/qt-creator/qt-creator/blob/master/LICENSES/LICENSE.GPL3-EXCEPT).
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) ![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41) ![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "RefactorSuggestion.hpp" #include "RefactorSuggestion.hpp"
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "RefactorSuggestionHoverHandler.hpp" #include "RefactorSuggestionHoverHandler.hpp"
#include "RefactorSuggestion.hpp" #include "RefactorSuggestion.hpp"

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FlowEditor.hpp" #include "FlowEditor.hpp"

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FlowItem.hpp" #include "FlowItem.hpp"
namespace QodeAssist::TaskFlow { namespace QodeAssist::TaskFlow {

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QQuickItem> #include <QQuickItem>

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FlowsModel.hpp" #include "FlowsModel.hpp"
#include "FlowManager.hpp" #include "FlowManager.hpp"

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QAbstractListModel> #include <QAbstractListModel>

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "GridBackground.hpp" #include "GridBackground.hpp"
#include <QPainter> #include <QPainter>

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "TaskConnectionItem.hpp" #include "TaskConnectionItem.hpp"
#include "TaskItem.hpp" #include "TaskItem.hpp"
#include "TaskPortItem.hpp" #include "TaskPortItem.hpp"

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include "TaskConnection.hpp" #include "TaskConnection.hpp"

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "TaskConnectionsModel.hpp" #include "TaskConnectionsModel.hpp"
namespace QodeAssist::TaskFlow { namespace QodeAssist::TaskFlow {

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QAbstractListModel> #include <QAbstractListModel>

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "TaskItem.hpp" #include "TaskItem.hpp"
namespace QodeAssist::TaskFlow { namespace QodeAssist::TaskFlow {

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QQuickItem> #include <QQuickItem>

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "TaskModel.hpp" #include "TaskModel.hpp"
namespace QodeAssist::TaskFlow { namespace QodeAssist::TaskFlow {

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QAbstractListModel> #include <QAbstractListModel>

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "TaskPortItem.hpp" #include "TaskPortItem.hpp"
namespace QodeAssist::TaskFlow { namespace QodeAssist::TaskFlow {

View File

@@ -1,7 +1,3 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <TaskPort.hpp> #include <TaskPort.hpp>

Some files were not shown because too many files have changed in this diff Show More