Compare commits

...

50 Commits

Author SHA1 Message Date
Petr Mironychev
2aa748b14a refactor: final Agent loader 2026-06-11 19:04:45 +02:00
Petr Mironychev
f499be278d test: Update tests 2026-06-11 15:38:12 +02:00
Petr Mironychev
231a6a0215 doc: update architecture 2026-06-11 15:28:37 +02:00
Petr Mironychev
69672deb45 refactor: IProjectScanner port; ContextManager QtC-free 2026-06-11 15:21:02 +02:00
Petr Mironychev
f36173d932 refactor: Agent roaster improve 2026-06-11 14:51:49 +02:00
Petr Mironychev
e65ac23e66 refactor: Move QuickRefactor to Session way 2026-06-11 14:18:44 +02:00
Petr Mironychev
7bfe9d6f0e refactor: Change to chat conversation 2026-06-11 14:06:19 +02:00
Petr Mironychev
05fe38e289 refactor: Remove project rules 2026-06-11 13:36:23 +02:00
Petr Mironychev
2c9475cddf fix: Code completion via session 2026-06-10 17:44:03 +02:00
Petr Mironychev
3179c0c358 refactor: add to template agent roles 2026-06-09 08:52:53 +02:00
Petr Mironychev
c151c5030b refactor: Finalize agent template 2026-06-09 08:48:32 +02:00
Petr Mironychev
98a618cf87 refactor: Add agents for providers 2026-06-09 08:48:32 +02:00
Petr Mironychev
6220308a93 refactor: Move to agent-session architecture 2026-06-09 08:46:45 +02:00
Petr Mironychev
02c11ee5a0 refactor: Remove experimental flag 2026-06-09 08:21:04 +02:00
Petr Mironychev
abb3351246 chore: Update version to 0.9.20 2026-06-08 15:51:03 +02:00
Petr Mironychev
57eeb32ceb chore: Update translations 2026-06-08 15:36:09 +02:00
Petr Mironychev
74eed49fb4 feat: Add transfer timeout settings 2026-06-08 15:20:21 +02:00
Petr Mironychev
43a30281b6 feat: Improve BusyIndicator 2026-06-08 12:53:37 +02:00
Petr Mironychev
bf4307c459 doc: Update License in README 2026-06-08 12:31:14 +02:00
Petr Mironychev
6df70e608b chore: Update LICENCE and copyright 2026-06-08 11:25:18 +02:00
Petr Mironychev
ee1bf4ffe5 feat: Improve chat, status and message sending keys (#361) 2026-06-06 11:25:30 +02:00
Petr Mironychev
aaca9e2a0b chore: Update plugin to 0.9.19 2026-06-01 12:09:25 +02:00
Petr Mironychev
f2aae9d37f fix: Using adaptive thinking for Claude opus 4.8 - 4.6 2026-06-01 12:08:03 +02:00
Petr Mironychev
dcf5796ad7 refactor: Move Qwen provider to separate classes 2026-05-29 12:49:32 +02:00
Petr Mironychev
033c0e8652 feat: Add DeepSeek provider 2026-05-29 12:41:18 +02:00
Petr Mironychev
ea67ba0e2a feat: Add Qwen provider 2026-05-29 11:33:06 +02:00
Petr Mironychev
0cf915c4a5 feat: Update dialog with update 2026-05-29 10:35:52 +02:00
Petr Mironychev
99caa853d5 chore: Update plugin to 0.9.18 version 2026-05-29 09:45:34 +02:00
Petr Mironychev
278624d412 fix: Prevent toolbar button clicks being eaten by focus guard 2026-05-29 09:44:44 +02:00
Petr Mironychev
f8adf4d264 fix: Clean request id by clear() 2026-05-28 17:58:33 +02:00
Petr Mironychev
bfcd8dc1fb fix: Prevent crash on cancelling quick refactor via progress widget 2026-05-28 16:00:18 +02:00
Petr Mironychev
33321b2499 Update README with extension registry example
Added an example of the extension registry with an image.
2026-05-28 14:40:28 +02:00
Petr Mironychev
362533a5c0 doc: Add installation from registry 2026-05-28 14:38:28 +02:00
Petr Mironychev
d180d189e4 chore: Update plugin to 0.9.17 version 2026-05-28 11:18:36 +02:00
Petr Mironychev
0774084ad9 fix: Add mouse propogation to qml chat in qquickwidget 2026-05-28 11:17:38 +02:00
Petr Mironychev
282f48d9fb fix: Improve parsing markdown 2026-05-28 10:50:25 +02:00
Petr Mironychev
8cbeb7132e fix: Replace context menu to system specific 2026-05-28 10:49:56 +02:00
Petr Mironychev
af898bd255 feat: Add preview to message navigator 2026-05-27 23:03:10 +02:00
Petr Mironychev
66e25300e8 tr: Update translations 2026-05-27 22:15:19 +02:00
Petr Mironychev
fcc651fd75 feat: Add message navigator 2026-05-27 22:06:41 +02:00
Petr Mironychev
dc016ce533 refactor: Improve file edit panel 2026-05-27 21:54:52 +02:00
Petr Mironychev
725de4a2c3 fix: Replace to custom tooltip 2026-05-27 19:23:28 +02:00
Petr Mironychev
8d3313d16b feat: Add translations to cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk 2026-05-27 18:42:56 +02:00
Petr Mironychev
abdcab3c7d fix: Add focus guard 2026-05-27 15:43:18 +02:00
Petr Mironychev
abadc2262c feat: Add session layout 2026-05-26 18:02:44 +02:00
Petr Mironychev
31ad99af61 refactor: Group sources cmake files to subfolder 2026-05-26 17:17:33 +02:00
Petr Mironychev
fb887967ed feat: Add agents pipelines 2026-05-26 16:44:45 +02:00
Petr Mironychev
97236c6069 feat: Add agents and agents settings 2026-05-26 12:31:00 +02:00
Petr Mironychev
51ebe3e523 Update README.md 2026-05-23 18:16:23 +02:00
Petr Mironychev
e193d1e1fa feat: Add settings page for providers (#353) 2026-05-21 19:30:32 +02:00
549 changed files with 65963 additions and 10230 deletions

View File

@@ -1,5 +1,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
list(APPEND CMAKE_PREFIX_PATH "/Users/palm1r/Qt/Qt Creator.sdk/lib/cmake/QtCreator")
project(QodeAssist) project(QodeAssist)
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
@@ -14,7 +16,9 @@ find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
find_package(GTest) find_package(GTest)
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en) qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES
en cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk
)
# IDE_VERSION is defined by QtCreator package # IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION}) string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
@@ -34,11 +38,9 @@ add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH} -DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
) )
add_subdirectory(sources/external/llmqore) add_subdirectory(sources)
add_subdirectory(sources/skills)
add_subdirectory(pluginllmcore)
add_subdirectory(settings)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(settings)
add_subdirectory(UIControls) add_subdirectory(UIControls)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context) add_subdirectory(context)
@@ -46,6 +48,11 @@ if(GTest_FOUND)
add_subdirectory(test) add_subdirectory(test)
endif() endif()
option(QODEASSIST_BUILD_BENCH "Build the standalone agent bench CLI" ON)
if(QODEASSIST_BUILD_BENCH)
add_subdirectory(bench)
endif()
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
QtCreator::Core QtCreator::Core
@@ -64,7 +71,8 @@ add_qtc_plugin(QodeAssist
QtCreator::Utils QtCreator::Utils
QtCreator::CPlusPlus QtCreator::CPlusPlus
LLMQore LLMQore
PluginLLMCore ProvidersConfig
Agents
Skills Skills
QodeAssistChatViewplugin QodeAssistChatViewplugin
SOURCES SOURCES
@@ -76,39 +84,6 @@ add_qtc_plugin(QodeAssist
QodeAssisttr.h QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
RefactorContextHelper.hpp RefactorContextHelper.hpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
templates/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/ProviderUrlUtils.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/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
@@ -120,7 +95,6 @@ add_qtc_plugin(QodeAssist
chat/ChatDocument.hpp chat/ChatDocument.cpp chat/ChatDocument.hpp chat/ChatDocument.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
ConfigurationManager.hpp ConfigurationManager.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
@@ -160,6 +134,8 @@ add_qtc_plugin(QodeAssist
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
@@ -181,5 +157,5 @@ endif()
qt_add_translations(TARGETS QodeAssist qt_add_translations(TARGETS QodeAssist
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
RESOURCE_PREFIX "/translations" RESOURCE_PREFIX "/translations"
LUPDATE_OPTIONS -no-obsolete LUPDATE_OPTIONS -no-obsolete -locations none
) )

View File

@@ -1,123 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "AgentRoleController.hpp"
#include <utils/aspects.h>
#include "AgentRole.hpp"
#include "ChatAssistantSettings.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat {
AgentRoleController::AgentRoleController(QObject *parent)
: QObject(parent)
{
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&AgentRoleController::baseSystemPromptChanged);
loadAvailableRoles();
}
QStringList AgentRoleController::availableRoles() const
{
return m_availableRoles;
}
QString AgentRoleController::currentRole() const
{
return m_currentRole;
}
QString AgentRoleController::baseSystemPrompt() const
{
return Settings::chatAssistantSettings().systemPrompt();
}
QString AgentRoleController::currentRoleDescription() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
return role.description;
}
QString AgentRoleController::currentRoleSystemPrompt() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return QString();
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return QString();
return role.systemPrompt;
}
void AgentRoleController::loadAvailableRoles()
{
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
m_availableRoles.clear();
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
for (const auto &role : roles)
m_availableRoles.append(role.name);
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
if (!lastRoleId.isEmpty()) {
for (const auto &role : roles) {
if (role.id == lastRoleId) {
m_currentRole = role.name;
break;
}
}
}
emit availableRolesChanged();
emit currentRoleChanged();
}
void AgentRoleController::applyRole(const QString &roleName)
{
auto &settings = Settings::chatAssistantSettings();
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
settings.lastUsedRoleId.setValue("");
settings.writeSettings();
m_currentRole = roleName;
emit currentRoleChanged();
return;
}
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
for (const auto &role : roles) {
if (role.name == roleName) {
settings.lastUsedRoleId.setValue(role.id);
settings.writeSettings();
m_currentRole = role.name;
emit currentRoleChanged();
break;
}
}
}
void AgentRoleController::openSettings()
{
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
}
} // namespace QodeAssist::Chat

View File

@@ -1,38 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QStringList>
namespace QodeAssist::Chat {
class AgentRoleController : public QObject
{
Q_OBJECT
public:
explicit AgentRoleController(QObject *parent = nullptr);
QStringList availableRoles() const;
QString currentRole() const;
QString baseSystemPrompt() const;
QString currentRoleDescription() const;
QString currentRoleSystemPrompt() const;
void loadAvailableRoles();
void applyRole(const QString &roleName);
void openSettings();
signals:
void availableRolesChanged();
void currentRoleChanged();
void baseSystemPromptChanged();
private:
QStringList m_availableRoles;
QString m_currentRole;
};
} // namespace QodeAssist::Chat

View File

@@ -27,6 +27,7 @@ qt_add_qml_module(QodeAssistChatView
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
@@ -44,6 +45,7 @@ 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/new-chat-icon.svg
icons/rules-icon.svg icons/rules-icon.svg
icons/context-icon.svg icons/context-icon.svg
@@ -58,6 +60,7 @@ qt_add_qml_module(QodeAssistChatView
icons/tools-icon-off.svg icons/tools-icon-off.svg
icons/settings-icon.svg icons/settings-icon.svg
icons/compress-icon.svg icons/compress-icon.svg
icons/open-in-code.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
@@ -72,8 +75,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
AgentRoleController.hpp AgentRoleController.cpp ChatAgentController.hpp ChatAgentController.cpp
ChatConfigurationController.hpp ChatConfigurationController.cpp
FileEditController.hpp FileEditController.cpp FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp ChatHistoryStore.hpp ChatHistoryStore.cpp
@@ -89,13 +91,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 Skills
Agents
Session
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView

View File

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

View File

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

View File

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

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::PluginLLMCore { namespace QodeAssist {
class Provider; class SessionManager;
class PromptTemplate; class Session;
} // namespace QodeAssist::PluginLLMCore class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject class ChatCompressor : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -24,7 +24,10 @@ class ChatCompressor : public QObject
public: public:
explicit ChatCompressor(QObject *parent = nullptr); explicit ChatCompressor(QObject *parent = nullptr);
void startCompression(const QString &chatFilePath, ChatModel *chatModel); void setSessionManager(SessionManager *sessionManager);
void setActiveAgent(const QString &agentName);
void startCompression(const QString &chatFilePath, ConversationHistory *sourceHistory);
bool isCompressing() const; bool isCompressing() const;
void cancelCompression(); void cancelCompression();
@@ -34,30 +37,23 @@ signals:
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error); void compressionFailed(const QString &error);
private slots:
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
void onFullResponseReceived(const QString &requestId, const QString &fullText);
void onRequestFailed(const QString &requestId, const QString &error);
private: private:
void onCompressionFinished(const QString &requestId);
void onCompressionFailed(const QString &requestId, const QString &error);
QString createCompressedChatPath(const QString &originalPath) const; QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const; QString buildCompressionPrompt() const;
bool createCompressedChatFile( bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary); const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState(); void cleanupState();
void handleCompressionError(const QString &error); void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false; bool m_isCompressing = false;
QString m_currentRequestId; QString m_currentRequestId;
QString m_originalChatPath; QString m_originalChatPath;
QString m_accumulatedSummary; QPointer<SessionManager> m_sessionManager;
PluginLLMCore::Provider *m_provider = nullptr; QString m_activeAgent;
ChatModel *m_chatModel = nullptr; QPointer<Session> m_session;
QList<QMetaObject::Connection> m_connections;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,99 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatConfigurationController.hpp"
#include <utils/aspects.h>
#include "ConfigurationManager.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat {
ChatConfigurationController::ChatConfigurationController(QObject *parent)
: QObject(parent)
{
auto &settings = Settings::generalSettings();
connect(
&settings.caProvider,
&Utils::BaseAspect::changed,
this,
&ChatConfigurationController::updateCurrentConfiguration);
connect(
&settings.caModel,
&Utils::BaseAspect::changed,
this,
&ChatConfigurationController::updateCurrentConfiguration);
loadAvailableConfigurations();
}
QStringList ChatConfigurationController::availableConfigurations() const
{
return m_availableConfigurations;
}
QString ChatConfigurationController::currentConfiguration() const
{
return m_currentConfiguration;
}
void ChatConfigurationController::updateCurrentConfiguration()
{
auto &settings = Settings::generalSettings();
m_currentConfiguration
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
emit currentConfigurationChanged();
}
void ChatConfigurationController::loadAvailableConfigurations()
{
auto &manager = Settings::ConfigurationManager::instance();
manager.loadConfigurations(Settings::ConfigurationType::Chat);
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::Chat);
m_availableConfigurations.clear();
m_availableConfigurations.append(QObject::tr("Current Settings"));
for (const Settings::AIConfiguration &config : configs) {
m_availableConfigurations.append(config.name);
}
updateCurrentConfiguration();
emit availableConfigurationsChanged();
}
void ChatConfigurationController::applyConfiguration(const QString &configName)
{
if (configName == QObject::tr("Current Settings")) {
return;
}
auto &manager = Settings::ConfigurationManager::instance();
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::Chat);
for (const Settings::AIConfiguration &config : configs) {
if (config.name == configName) {
auto &settings = Settings::generalSettings();
settings.caProvider.setValue(config.provider);
settings.caModel.setValue(config.model);
settings.caTemplate.setValue(config.templateName);
settings.caUrl.setValue(config.url);
settings.caCustomEndpoint.setValue(config.customEndpoint);
settings.writeSettings();
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
emit currentConfigurationChanged();
break;
}
}
}
} // namespace QodeAssist::Chat

View File

@@ -1,35 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QStringList>
namespace QodeAssist::Chat {
class ChatConfigurationController : public QObject
{
Q_OBJECT
public:
explicit ChatConfigurationController(QObject *parent = nullptr);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
void loadAvailableConfigurations();
void applyConfiguration(const QString &configName);
signals:
void availableConfigurationsChanged();
void currentConfigurationChanged();
private:
void updateCurrentConfiguration();
QStringList m_availableConfigurations;
QString m_currentConfiguration;
};
} // namespace QodeAssist::Chat

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatHistoryStore.hpp" #include "ChatHistoryStore.hpp"
@@ -15,15 +16,20 @@
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include "ChatModel.hpp" #include <ConversationHistory.hpp>
#include <Message.hpp>
#include <PluginBlocks.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent) ChatHistoryStore::ChatHistoryStore(ConversationHistory *history, QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel) , m_history(history)
{} {}
QString ChatHistoryStore::historyDir() const QString ChatHistoryStore::historyDir() const
@@ -51,17 +57,23 @@ QString ChatHistoryStore::suggestedFileName() const
{ {
QString shortMessage; QString shortMessage;
if (m_chatModel->rowCount() > 0) { if (m_history) {
QString firstMessage for (const auto &message : m_history->messages()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString(); if (message.role() != Message::Role::User)
shortMessage = firstMessage.split('\n').first().simplified().left(30); continue;
if (shortMessage.isEmpty()) { const QString text = message.text();
QVariantList images if (!text.trimmed().isEmpty()) {
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList(); shortMessage = text.split('\n').first().simplified().left(30);
if (!images.isEmpty()) { } else {
shortMessage = "image_chat"; for (const auto &block : message.blocks()) {
if (dynamic_cast<StoredImageContent *>(block.get())) {
shortMessage = "image_chat";
break;
}
}
} }
break;
} }
} }
@@ -106,12 +118,12 @@ QString ChatHistoryStore::autosaveFilePath(
SerializationResult ChatHistoryStore::save(const QString &filePath) const SerializationResult ChatHistoryStore::save(const QString &filePath) const
{ {
return ChatSerializer::saveToFile(m_chatModel, filePath); return ChatSerializer::saveToFile(m_history, filePath);
} }
SerializationResult ChatHistoryStore::load(const QString &filePath) const SerializationResult ChatHistoryStore::load(const QString &filePath) const
{ {
return ChatSerializer::loadFromFile(m_chatModel, filePath); return ChatSerializer::loadFromFile(m_history, filePath);
} }
void ChatHistoryStore::showSaveDialog() void ChatHistoryStore::showSaveDialog()

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -8,16 +9,18 @@
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
namespace QodeAssist::Chat { namespace QodeAssist {
class ConversationHistory;
}
class ChatModel; namespace QodeAssist::Chat {
class ChatHistoryStore : public QObject class ChatHistoryStore : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr); explicit ChatHistoryStore(ConversationHistory *history, QObject *parent = nullptr);
QString historyDir() const; QString historyDir() const;
QString suggestedFileName() const; QString suggestedFileName() const;
@@ -41,7 +44,7 @@ signals:
private: private:
QString generateChatFileName(const QString &shortMessage, const QString &dir) const; QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
ChatModel *m_chatModel; ConversationHistory *m_history;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -7,11 +8,16 @@
#include "MessagePart.hpp" #include "MessagePart.hpp"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QHash>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QPointer>
#include <QVector>
#include <QtQmlIntegration> #include <QtQmlIntegration>
#include "context/ContentFile.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -42,79 +48,18 @@ public:
}; };
Q_ENUM(Roles) Q_ENUM(Roles)
struct ImageAttachment
{
QString fileName; // Original filename
QString storedPath; // Path to stored image file (relative to chat folder)
QString mediaType; // MIME type
};
struct Message
{
ChatRole role;
QString content;
QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments;
QList<ImageAttachment> images;
QString toolName;
QJsonObject toolArguments;
QString toolResult;
int promptTokens = 0;
int completionTokens = 0;
int cachedPromptTokens = 0;
int reasoningTokens = 0;
};
explicit ChatModel(QObject *parent = nullptr); explicit ChatModel(QObject *parent = nullptr);
void setHistory(ConversationHistory *history);
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {},
bool isRedacted = false,
const QString &signature = QString());
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index); Q_INVOKABLE void resetModelTo(int index);
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
void addToolExecutionStatus(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments);
void dropTrailingAssistantMessage(const QString &requestId);
void setToolMessageData(
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments,
const QString &toolResult);
void updateToolResult(
const QString &requestId,
const QString &toolId,
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);
void setMessageUsage( void setMessageUsage(
const QString &messageId, const QString &messageId,
@@ -127,10 +72,7 @@ public:
int sessionCompletionTokens() const; int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const; int sessionCachedPromptTokens() const;
int sessionTotalTokens() const; int sessionTotalTokens() const;
void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
@@ -139,18 +81,60 @@ signals:
void sessionUsageChanged(); void sessionUsageChanged();
private slots: private slots:
void onFileEditApplied(const QString &editId); void onHistoryMessageAdded(int index);
void onFileEditRejected(const QString &editId); void onHistoryMessageUpdated(int index);
void onFileEditArchived(const QString &editId); void onHistoryCleared();
void onHistoryReset();
void onFileEditStatusChanged(const QString &editId);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage); struct AttachmentRef
{
QVector<Message> m_messages; QString fileName;
bool m_loadingFromHistory = false; 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();
void reprojectTail(int startMessageIndex);
int startMessageIndexFor(int messageIndex) const;
int firstRowForMessage(int messageIndex) const;
QHash<QString, QString> buildToolResultMap() const;
void appendRowsForMessage(
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const;
QString overlayFileEditStatus(const QString &content, const QString &editId) const;
QVariantList buildAttachmentList(const QVector<AttachmentRef> &attachments) const;
QVariantList buildImageList(const QVector<ImageRef> &images) const;
QPointer<ConversationHistory> m_history;
QVector<Row> m_rows;
QHash<QString, Usage> m_usageByMessageId;
QString m_chatFilePath; QString m_chatFilePath;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart) Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatRootView.hpp" #include "ChatRootView.hpp"
@@ -10,6 +11,7 @@
#include <QFile> #include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
#include <QKeySequence>
#include <QMessageBox> #include <QMessageBox>
#include <QQmlContext> #include <QQmlContext>
#include <QQmlEngine> #include <QQmlEngine>
@@ -26,9 +28,16 @@
#include "QodeAssistConstants.hpp" #include "QodeAssistConstants.hpp"
#include "AgentRoleController.hpp" #include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <Message.hpp>
#include <SessionManager.hpp>
#include <sources/settings/PipelinesConfig.hpp>
#include "ChatAgentController.hpp"
#include "AgentRole.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatConfigurationController.hpp"
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
#include "ChatHistoryStore.hpp" #include "ChatHistoryStore.hpp"
#include "FileEditController.hpp" #include "FileEditController.hpp"
@@ -36,10 +45,8 @@
#include "InputTokenCounter.hpp" #include "InputTokenCounter.hpp"
#include "SettingsConstants.hpp" #include "SettingsConstants.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "SessionFileRegistry.hpp" #include "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "pluginllmcore/RulesLoader.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "SkillsSettings.hpp" #include "SkillsSettings.hpp"
#include "sources/skills/SkillsManager.hpp" #include "sources/skills/SkillsManager.hpp"
@@ -52,23 +59,39 @@ bool isChatEditor(Core::IEditor *editor)
return editor && editor->document() return editor && editor->document()
&& editor->document()->id() == Utils::Id(Constants::QODE_ASSIST_CHAT_EDITOR_ID); && editor->document()->id() == Utils::Id(Constants::QODE_ASSIST_CHAT_EDITOR_ID);
} }
QKeySequence sendMessageKeySequence()
{
auto command = Core::ActionManager::command(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE);
if (!command)
return {};
QKeySequence sequence = command->keySequence();
if (sequence.isEmpty()) {
const QList<QKeySequence> defaults = command->defaultKeySequences();
if (!defaults.isEmpty())
sequence = defaults.constFirst();
}
return sequence;
}
} // namespace } // namespace
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_history(new QodeAssist::ConversationHistory(this))
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, this))
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_fileManager(new ChatFileManager(this)) , m_fileManager(new ChatFileManager(this))
, m_isRequestInProgress(false) , m_isRequestInProgress(false)
, m_chatCompressor(new ChatCompressor(this)) , m_chatCompressor(new ChatCompressor(this))
, m_agentRoleController(new AgentRoleController(this)) , m_agentController(new ChatAgentController(this))
, m_configurationController(new ChatConfigurationController(this)) , m_fileEditController(new FileEditController(this))
, m_fileEditController(new FileEditController(m_chatModel, this)) , m_tokenCounter(new InputTokenCounter(m_history, m_clientInterface->contextManager(), this))
, m_tokenCounter( , m_historyStore(new ChatHistoryStore(m_history, this))
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
{ {
m_chatModel->setHistory(m_history);
m_clientInterface->setHistory(m_history);
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles(); m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect( connect(
&Settings::chatAssistantSettings().linkOpenFiles, &Settings::chatAssistantSettings().linkOpenFiles,
@@ -76,21 +99,21 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); }); [this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); });
auto &settings = Settings::generalSettings(); QMetaObject::invokeMethod(
connect(
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
connect(
m_configurationController,
&ChatConfigurationController::availableConfigurationsChanged,
this, this,
&ChatRootView::availableConfigurationsChanged); [this] {
connect( if (auto sendCommand
m_configurationController, = Core::ActionManager::command(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE)) {
&ChatConfigurationController::currentConfigurationChanged, connect(
this, sendCommand,
&ChatRootView::currentConfigurationChanged); &Core::Command::keySequenceChanged,
this,
&ChatRootView::sendShortcutTextChanged,
Qt::UniqueConnection);
}
emit sendShortcutTextChanged();
},
Qt::QueuedConnection);
connect( connect(
m_clientInterface, m_clientInterface,
@@ -138,20 +161,30 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::inputTokensCountChanged); &ChatRootView::inputTokensCountChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::availableRolesChanged, &ChatAgentController::availableAgentsChanged,
this, this,
&ChatRootView::availableAgentRolesChanged); &ChatRootView::availableChatAgentsChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::currentRoleChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::currentAgentRoleChanged); &ChatRootView::currentChatAgentChanged);
connect( connect(
m_agentRoleController, m_agentController,
&AgentRoleController::baseSystemPromptChanged, &ChatAgentController::currentAgentChanged,
this, this,
&ChatRootView::baseSystemPromptChanged); &ChatRootView::isThinkingSupportChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useToolsChanged);
connect(
m_agentController,
&ChatAgentController::currentAgentChanged,
this,
&ChatRootView::useThinkingChanged);
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@@ -233,14 +266,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect( connect(
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory); m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
refreshRules();
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
connect( connect(
ProjectExplorer::ProjectManager::instance(), ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded, &ProjectExplorer::ProjectManager::projectAdded,
@@ -265,12 +290,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::useThinkingChanged); &ChatRootView::useThinkingChanged);
connect(
&Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isThinkingSupportChanged);
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) { connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
m_lastErrorMessage = error; m_lastErrorMessage = error;
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
@@ -291,7 +310,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) { if (m_pendingSend.active) {
PendingSend p = m_pendingSend; PendingSend p = m_pendingSend;
m_pendingSend = {}; m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking); dispatchSend(p.message, p.attachments, p.linkedFiles);
} }
}); });
@@ -304,7 +323,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
if (m_pendingSend.active) { if (m_pendingSend.active) {
PendingSend p = m_pendingSend; PendingSend p = m_pendingSend;
m_pendingSend = {}; m_pendingSend = {};
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking); dispatchSend(p.message, p.attachments, p.linkedFiles);
} }
}); });
} }
@@ -340,6 +359,85 @@ Skills::SkillsManager *ChatRootView::skillsManager() const
return m_skillsManager; return m_skillsManager;
} }
AgentFactory *ChatRootView::agentFactory() const
{
if (!m_agentFactory) {
if (auto *engine = qmlEngine(this)) {
m_agentFactory = qobject_cast<AgentFactory *>(
engine->rootContext()->contextProperty("agentFactory").value<QObject *>());
}
}
return m_agentFactory;
}
SessionManager *ChatRootView::sessionManager() const
{
if (!m_sessionManager) {
if (auto *engine = qmlEngine(this)) {
m_sessionManager = qobject_cast<SessionManager *>(
engine->rootContext()->contextProperty("sessionManager").value<QObject *>());
}
}
return m_sessionManager;
}
void ChatRootView::loadAvailableChatAgents()
{
m_agentController->setAgentFactory(agentFactory());
}
QStringList ChatRootView::availableChatAgents() const
{
return m_agentController->availableAgents();
}
QString ChatRootView::currentChatAgent() const
{
return m_agentController->currentAgent();
}
void ChatRootView::setCurrentChatAgent(const QString &name)
{
m_agentController->setCurrentAgent(name);
}
QStringList ChatRootView::availableRoles() const
{
return m_availableRoles;
}
QString ChatRootView::currentRole() const
{
return m_currentRole;
}
void ChatRootView::setCurrentRole(const QString &roleId)
{
if (m_currentRole == roleId)
return;
m_currentRole = roleId;
emit currentRoleChanged();
}
void ChatRootView::loadAvailableRoles()
{
QStringList ids;
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
ids.reserve(roles.size());
for (const auto &r : roles)
ids << r.id;
if (ids != m_availableRoles) {
m_availableRoles = ids;
emit availableRolesChanged();
}
if (!m_availableRoles.isEmpty() && !m_availableRoles.contains(m_currentRole))
setCurrentRole(m_availableRoles.contains(QStringLiteral("developer"))
? QStringLiteral("developer")
: m_availableRoles.first());
}
QVariantList ChatRootView::searchSkills(const QString &query) const QVariantList ChatRootView::searchSkills(const QString &query) const
{ {
QVariantList results; QVariantList results;
@@ -347,7 +445,7 @@ QVariantList ChatRootView::searchSkills(const QString &query) const
if (!manager || !Settings::skillsSettings().enableSkills()) if (!manager || !Settings::skillsSettings().enableSkills())
return results; return results;
auto *project = PluginLLMCore::RulesLoader::getActiveProject(); auto *project = ProjectExplorer::ProjectManager::startupProject();
QStringList projectSkillDirs; QStringList projectSkillDirs;
if (project) { if (project) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
@@ -383,21 +481,17 @@ void ChatRootView::sendMessage(const QString &message)
{ {
const QStringList attachments = m_attachmentFiles; const QStringList attachments = m_attachmentFiles;
const QStringList linkedFiles = m_linkedFiles; const QStringList linkedFiles = m_linkedFiles;
const bool tools = useTools();
const bool thinking = useThinking();
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking)) if (deferSendForAutoCompress(message, attachments, linkedFiles))
return; return;
dispatchSend(message, attachments, linkedFiles, tools, thinking); dispatchSend(message, attachments, linkedFiles);
} }
bool ChatRootView::deferSendForAutoCompress( bool ChatRootView::deferSendForAutoCompress(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles)
bool useToolsArg,
bool useThinkingArg)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
if (!settings.autoCompress()) if (!settings.autoCompress())
@@ -423,7 +517,7 @@ bool ChatRootView::deferSendForAutoCompress(
.arg(inputTokens) .arg(inputTokens)
.arg(threshold)); .arg(threshold));
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true}; m_pendingSend = {message, attachments, linkedFiles, true};
compressCurrentChat(); compressCurrentChat();
return true; return true;
} }
@@ -431,9 +525,7 @@ bool ChatRootView::deferSendForAutoCompress(
void ChatRootView::dispatchSend( void ChatRootView::dispatchSend(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles)
bool useToolsArg,
bool useThinkingArg)
{ {
if (m_recentFilePath.isEmpty()) { if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath(message, attachments); QString filePath = getAutosaveFilePath(message, attachments);
@@ -448,8 +540,14 @@ void ChatRootView::dispatchSend(
m_tokenCounter->recordSent(); m_tokenCounter->recordSent();
if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
m_clientInterface->setSkillsManager(skillsManager()); m_clientInterface->setSkillsManager(skillsManager());
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg); m_clientInterface->setSessionManager(sessionManager());
m_clientInterface->setActiveAgent(currentChatAgent());
m_clientInterface->setActiveRole(currentRole());
m_clientInterface->sendMessage(message, attachments, linkedFiles);
m_fileManager->clearIntermediateStorage(); m_fileManager->clearIntermediateStorage();
clearAttachmentFiles(); clearAttachmentFiles();
@@ -494,12 +592,6 @@ void ChatRootView::clearMessages()
clearLinkedFiles(); clearLinkedFiles();
} }
QString ChatRootView::currentTemplate() const
{
auto &settings = Settings::generalSettings();
return settings.caModel();
}
void ChatRootView::saveHistory(const QString &filePath) void ChatRootView::saveHistory(const QString &filePath)
{ {
if (filePath != m_recentFilePath) { if (filePath != m_recentFilePath) {
@@ -743,6 +835,32 @@ void ChatRootView::calculateMessageTokensCount(const QString &message)
m_tokenCounter->setMessage(message); m_tokenCounter->setMessage(message);
} }
bool ChatRootView::isSendShortcut(int key, int modifiers) const
{
const QKeySequence sequence = sendMessageKeySequence();
if (sequence.isEmpty())
return false;
const QKeyCombination combination = sequence[0];
const int sequenceKey = combination.key();
const int relevantMask = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier
| Qt::MetaModifier;
const int sequenceModifiers = combination.keyboardModifiers() & relevantMask;
const int eventModifiers = modifiers & relevantMask;
const bool isReturnLike = sequenceKey == Qt::Key_Return || sequenceKey == Qt::Key_Enter;
const bool keyMatches = key == sequenceKey
|| (isReturnLike && (key == Qt::Key_Return || key == Qt::Key_Enter));
return keyMatches && eventModifiers == sequenceModifiers;
}
QString ChatRootView::sendShortcutText() const
{
return sendMessageKeySequence().toString(QKeySequence::NativeText);
}
void ChatRootView::setIsSyncOpenFiles(bool state) void ChatRootView::setIsSyncOpenFiles(bool state)
{ {
if (m_isSyncOpenFiles != state) { if (m_isSyncOpenFiles != state) {
@@ -762,25 +880,6 @@ void ChatRootView::openChatHistoryFolder()
m_historyStore->openHistoryFolder(); m_historyStore->openHistoryFolder();
} }
void ChatRootView::openRulesFolder()
{
auto project = ProjectExplorer::ProjectManager::startupProject();
if (!project) {
return;
}
QString projectPath = project->projectDirectory().toFSPathString();
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
QDir dir(rulesPath);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
void ChatRootView::openSettings() void ChatRootView::openSettings()
{ {
QMetaObject::invokeMethod( QMetaObject::invokeMethod(
@@ -831,13 +930,12 @@ QString ChatRootView::chatTitle() const
QString ChatRootView::computeChatTitle() const QString ChatRootView::computeChatTitle() const
{ {
if (!m_chatModel) if (!m_history)
return {}; return {};
const auto history = m_chatModel->getChatHistory(); for (const auto &msg : m_history->messages()) {
for (const auto &msg : history) { if (msg.role() != Message::Role::User)
if (msg.role != ChatModel::User)
continue; continue;
const QString content = msg.content.trimmed(); const QString content = msg.text().trimmed();
if (content.isEmpty()) if (content.isEmpty())
continue; continue;
const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed(); const QString firstLine = content.section(QChar('\n'), 0, 0).trimmed();
@@ -1005,11 +1103,7 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath) bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{ {
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath); if (m_clientInterface->contextManager()->shouldIgnore(filePath.toFSPathString())) {
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1") LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString())); .arg(filePath.toFSPathString()));
return true; return true;
@@ -1061,71 +1155,14 @@ QString ChatRootView::lastErrorMessage() const
return m_lastErrorMessage; return m_lastErrorMessage;
} }
QVariantList ChatRootView::activeRules() const
{
return m_activeRules;
}
int ChatRootView::activeRulesCount() const
{
return m_activeRules.size();
}
QString ChatRootView::getRuleContent(int index)
{
if (index < 0 || index >= m_activeRules.size())
return QString();
return PluginLLMCore::RulesLoader::loadRuleFileContent(
m_activeRules[index].toMap()["filePath"].toString());
}
void ChatRootView::refreshRules()
{
m_activeRules.clear();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (!project) {
emit activeRulesChanged();
emit activeRulesCountChanged();
return;
}
auto ruleFiles
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
for (const auto &ruleFile : ruleFiles) {
QVariantMap ruleMap;
ruleMap["filePath"] = ruleFile.filePath;
ruleMap["fileName"] = ruleFile.fileName;
ruleMap["category"] = ruleFile.category;
m_activeRules.append(ruleMap);
}
emit activeRulesChanged();
emit activeRulesCountChanged();
}
bool ChatRootView::useTools() const bool ChatRootView::useTools() const
{ {
return Settings::chatAssistantSettings().enableChatTools(); return m_agentController->currentSupportsTools();
}
void ChatRootView::setUseTools(bool enabled)
{
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
} }
bool ChatRootView::useThinking() const bool ChatRootView::useThinking() const
{ {
return Settings::chatAssistantSettings().enableThinkingMode(); return m_agentController->currentSupportsThinking();
}
void ChatRootView::setUseThinking(bool enabled)
{
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
Settings::chatAssistantSettings().writeSettings();
} }
void ChatRootView::applyFileEdit(const QString &editId) void ChatRootView::applyFileEdit(const QString &editId)
@@ -1190,10 +1227,7 @@ QString ChatRootView::lastInfoMessage() const
bool ChatRootView::isThinkingSupport() const bool ChatRootView::isThinkingSupport() const
{ {
auto providerName = Settings::generalSettings().caProvider(); return m_agentController->currentSupportsThinking();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
} }
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
@@ -1214,66 +1248,6 @@ bool ChatRootView::isImageFile(const QString &filePath) const
return imageExtensions.contains(fileInfo.suffix().toLower()); return imageExtensions.contains(fileInfo.suffix().toLower());
} }
void ChatRootView::loadAvailableConfigurations()
{
m_configurationController->loadAvailableConfigurations();
}
void ChatRootView::applyConfiguration(const QString &configName)
{
m_configurationController->applyConfiguration(configName);
}
QStringList ChatRootView::availableConfigurations() const
{
return m_configurationController->availableConfigurations();
}
QString ChatRootView::currentConfiguration() const
{
return m_configurationController->currentConfiguration();
}
void ChatRootView::loadAvailableAgentRoles()
{
m_agentRoleController->loadAvailableRoles();
}
void ChatRootView::applyAgentRole(const QString &roleName)
{
m_agentRoleController->applyRole(roleName);
}
QStringList ChatRootView::availableAgentRoles() const
{
return m_agentRoleController->availableRoles();
}
QString ChatRootView::currentAgentRole() const
{
return m_agentRoleController->currentRole();
}
QString ChatRootView::baseSystemPrompt() const
{
return m_agentRoleController->baseSystemPrompt();
}
QString ChatRootView::currentAgentRoleDescription() const
{
return m_agentRoleController->currentRoleDescription();
}
QString ChatRootView::currentAgentRoleSystemPrompt() const
{
return m_agentRoleController->currentRoleSystemPrompt();
}
void ChatRootView::openAgentRolesSettings()
{
m_agentRoleController->openSettings();
}
void ChatRootView::compressCurrentChat() void ChatRootView::compressCurrentChat()
{ {
if (m_chatCompressor->isCompressing()) { if (m_chatCompressor->isCompressing()) {
@@ -1290,7 +1264,20 @@ void ChatRootView::compressCurrentChat()
autosave(); autosave();
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel); if (currentChatAgent().isEmpty())
loadAvailableChatAgents();
m_chatCompressor->setSessionManager(sessionManager());
QString compressionAgent = currentChatAgent();
const QStringList roster = Settings::PipelinesConfig::load().rosters.chatCompression;
if (!roster.isEmpty() && agentFactory()) {
const QString picked
= AgentRouter::pickAgent(roster, AgentRouter::Context{}, *agentFactory());
if (!picked.isEmpty())
compressionAgent = picked;
}
m_chatCompressor->setActiveAgent(compressionAgent);
m_chatCompressor->startCompression(m_recentFilePath, m_history);
} }
void ChatRootView::cancelCompression() void ChatRootView::cancelCompression()

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -10,18 +11,22 @@
#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 { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
namespace QodeAssist {
class AgentFactory;
class SessionManager;
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor; class ChatCompressor;
class AgentRoleController; class ChatAgentController;
class ChatConfigurationController;
class FileEditController; class FileEditController;
class InputTokenCounter; class InputTokenCounter;
class ChatHistoryStore; class ChatHistoryStore;
@@ -31,7 +36,6 @@ 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)
@@ -45,23 +49,19 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL) Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL) Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL) Q_PROPERTY(bool useTools READ useTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL) Q_PROPERTY(bool useThinking READ useThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL) Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL) Q_PROPERTY(QStringList availableRoles READ availableRoles NOTIFY availableRolesChanged FINAL)
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL) Q_PROPERTY(QString currentRole READ currentRole WRITE setCurrentRole NOTIFY currentRoleChanged FINAL)
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL) Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL) Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
@@ -73,7 +73,6 @@ public:
~ChatRootView() override; ~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);
@@ -98,9 +97,10 @@ 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);
@@ -135,18 +135,11 @@ public:
void setRequestProgressStatus(bool state); void setRequestProgressStatus(bool state);
QString lastErrorMessage() const; QString lastErrorMessage() const;
QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
Q_INVOKABLE QVariantList searchSkills(const QString &query) const; Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
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);
@@ -157,23 +150,19 @@ public:
Q_INVOKABLE void undoAllFileEditsForCurrentMessage(); Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats(); Q_INVOKABLE void updateCurrentMessageEditsStats();
Q_INVOKABLE void loadAvailableConfigurations();
Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
Q_INVOKABLE void compressCurrentChat(); Q_INVOKABLE void compressCurrentChat();
Q_INVOKABLE void cancelCompression(); Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles(); Q_INVOKABLE void loadAvailableChatAgents();
Q_INVOKABLE void applyAgentRole(const QString &roleId); QStringList availableChatAgents() const;
Q_INVOKABLE void openAgentRolesSettings(); QString currentChatAgent() const;
QStringList availableAgentRoles() const; void setCurrentChatAgent(const QString &name);
QString currentAgentRole() const;
QString baseSystemPrompt() const; Q_INVOKABLE void loadAvailableRoles();
QString currentAgentRoleDescription() const; QStringList availableRoles() const;
QString currentAgentRoleSystemPrompt() const; QString currentRole() const;
void setCurrentRole(const QString &roleId);
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const; int currentMessagePendingEdits() const;
@@ -202,7 +191,6 @@ public slots:
signals: signals:
void chatModelChanged(); void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged(); void attachmentFilesChanged();
void linkedFilesChanged(); void linkedFilesChanged();
void inputTokensCountChanged(); void inputTokensCountChanged();
@@ -218,20 +206,18 @@ signals:
void lastErrorMessageChanged(); void lastErrorMessageChanged();
void lastInfoMessageChanged(); void lastInfoMessageChanged();
void activeRulesChanged(); void sendShortcutTextChanged();
void activeRulesCountChanged();
void useToolsChanged(); void useToolsChanged();
void useThinkingChanged(); void useThinkingChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged(); void isThinkingSupportChanged();
void availableConfigurationsChanged();
void currentConfigurationChanged();
void availableAgentRolesChanged(); void availableChatAgentsChanged();
void currentAgentRoleChanged(); void currentChatAgentChanged();
void baseSystemPromptChanged(); void availableRolesChanged();
void currentRoleChanged();
void isCompressingChanged(); void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
@@ -251,25 +237,22 @@ private:
bool deferSendForAutoCompress( bool deferSendForAutoCompress(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles);
bool useTools,
bool useThinking);
void dispatchSend( void dispatchSend(
const QString &message, const QString &message,
const QStringList &attachments, const QStringList &attachments,
const QStringList &linkedFiles, const QStringList &linkedFiles);
bool useTools,
bool useThinking);
bool hasImageAttachments(const QStringList &attachments) const; bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const; SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() 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;
@@ -278,8 +261,6 @@ private:
QString message; QString message;
QStringList attachments; QStringList attachments;
QStringList linkedFiles; QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false; bool active = false;
}; };
PendingSend m_pendingSend; PendingSend m_pendingSend;
@@ -289,13 +270,14 @@ private:
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_lastInfoMessage; QString m_lastInfoMessage;
QString m_currentRole = QStringLiteral("developer");
QStringList m_availableRoles;
ChatCompressor *m_chatCompressor; ChatCompressor *m_chatCompressor;
AgentRoleController *m_agentRoleController; ChatAgentController *m_agentController;
ChatConfigurationController *m_configurationController;
FileEditController *m_fileEditController; FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter; InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore; ChatHistoryStore *m_historyStore;
@@ -303,6 +285,8 @@ private:
mutable bool m_sessionFileRegistryResolved = false; mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager; mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false; mutable bool m_skillsManagerResolved = false;
mutable QPointer<AgentFactory> m_agentFactory;
mutable QPointer<SessionManager> m_sessionManager;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

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

View File

@@ -1,13 +1,15 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include "ChatModel.hpp" namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -20,26 +22,26 @@ struct SerializationResult
class ChatSerializer class ChatSerializer
{ {
public: public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath); static SerializationResult saveToFile(
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath); const ConversationHistory *history, const QString &filePath);
static SerializationResult loadFromFile(ConversationHistory *history, const QString &filePath);
// Public for testing purposes
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
// Content management (images and text files) // Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath); static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage(const QString &chatFilePath, static bool saveContentToStorage(
const QString &fileName, const QString &chatFilePath,
const QString &base64Data, const QString &fileName,
QString &storedPath); const QString &base64Data,
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,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatUtils.h" #include "ChatUtils.h"
@@ -19,22 +20,34 @@ QString ChatUtils::getSafeMarkdownText(const QString &text) const
return text; return text;
} }
bool needsSanitization = false;
for (const QChar &ch : text) {
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
needsSanitization = true;
break;
}
}
if (!needsSanitization) {
return text;
}
QString safeText; QString safeText;
safeText.reserve(text.size()); safeText.reserve(text.size() + 16);
bool inFenced = false;
bool inInline = false;
for (int i = 0; i < text.size(); ++i) {
const QChar ch = text[i];
if (!inInline && i + 2 < text.size()
&& text[i] == '`' && text[i + 1] == '`' && text[i + 2] == '`') {
safeText.append(QStringLiteral("```"));
inFenced = !inFenced;
i += 2;
continue;
}
if (!inFenced && ch == '`') {
safeText.append(ch);
inInline = !inInline;
continue;
}
if (!inFenced && !inInline && ch == '<') {
safeText.append(QStringLiteral("&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,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatWidget.hpp" #include "ChatWidget.hpp"
#include <QApplication> #include <QApplication>
#include <QMouseEvent>
#include <QQmlContext> #include <QQmlContext>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQuickItem> #include <QQuickItem>
@@ -38,6 +40,8 @@ ChatWidget::ChatWidget(
setResizeMode(QQuickWidget::SizeRootObjectToView); setResizeMode(QQuickWidget::SizeRootObjectToView);
setFocusPolicy(Qt::StrongFocus); setFocusPolicy(Qt::StrongFocus);
setAttribute(Qt::WA_NoMousePropagation, true);
if (registerOwnContext) { if (registerOwnContext) {
auto ideContext = new Core::IContext{this}; auto ideContext = new Core::IContext{this};
ideContext->setWidget(this); ideContext->setWidget(this);
@@ -53,6 +57,14 @@ void ChatWidget::focusInEvent(QFocusEvent *event)
QMetaObject::invokeMethod(rootObject(), "focusInput"); QMetaObject::invokeMethod(rootObject(), "focusInput");
} }
void ChatWidget::mousePressEvent(QMouseEvent *event)
{
if (!hasFocus())
setFocus(Qt::MouseFocusReason);
QQuickWidget::mousePressEvent(event);
}
void ChatWidget::clear() void ChatWidget::clear()
{ {
QMetaObject::invokeMethod(rootObject(), "clearChat"); QMetaObject::invokeMethod(rootObject(), "clearChat");

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -42,6 +43,7 @@ signals:
protected: protected:
void focusInEvent(QFocusEvent *event) override; void focusInEvent(QFocusEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,75 +1,107 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <LLMQore/BaseClient.hpp> #include <memory>
#include <vector>
#include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h>
#include <projectexplorer/buildconfiguration.h> #include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/target.h> #include <projectexplorer/target.h>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QImageReader>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QRegularExpression> #include <QRegularExpression>
#include <QUuid> #include <QUuid>
#include <coreplugin/editormanager/editormanager.h> #include <ConversationHistory.hpp>
#include <coreplugin/editormanager/ieditor.h> #include <ContextRenderer.hpp>
#include <coreplugin/idocument.h> #include <Message.hpp>
#include <projectexplorer/project.h> #include <PluginBlocks.hpp>
#include <projectexplorer/projectexplorer.h> #include <Session.hpp>
#include <projectexplorer/projectmanager.h> #include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <LLMQore/ToolsManager.hpp>
#include "tools/ReadOriginalHistoryTool.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 "ProjectSettings.hpp"
#include "ProvidersManager.hpp"
#include "SkillsSettings.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> #include <sources/skills/SkillsManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( namespace {
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent) struct StoredImage
{
QString fileName;
QString storedPath;
QString mediaType;
};
} // namespace
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
: QObject(parent) : QObject(parent)
, m_promptProvider(promptProvider)
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_contextManager(new Context::ContextManager(this)) , m_contextManager(new Context::ContextManager(this))
{} {}
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
{
m_skillsManager = skillsManager;
}
ClientInterface::~ClientInterface() 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()) { if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message"); LOG_MESSAGE("Ignoring empty chat message");
@@ -77,19 +109,16 @@ void ClientInterface::sendMessage(
} }
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;
@@ -111,24 +140,19 @@ void ClientInterface::sendMessage(
.arg(textFiles.size())); .arg(textFiles.size()));
} }
QList<ChatModel::ImageAttachment> imageAttachments; QList<StoredImage> storedImages;
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const QString &imagePath : imageFiles) { for (const QString &imagePath : imageFiles) {
QString base64Data = encodeImageToBase64(imagePath); QString base64Data = encodeImageToBase64(imagePath);
if (base64Data.isEmpty()) { if (base64Data.isEmpty())
continue; continue;
}
QString storedPath; QString storedPath;
QFileInfo fileInfo(imagePath); QFileInfo fileInfo(imagePath);
if (ChatSerializer::saveContentToStorage( if (ChatSerializer::saveContentToStorage(
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) { m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
ChatModel::ImageAttachment imageAttachment; storedImages.append(
imageAttachment.fileName = fileInfo.fileName(); {fileInfo.fileName(), storedPath, getMediaTypeForImage(imagePath)});
imageAttachment.storedPath = storedPath;
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
imageAttachments.append(imageAttachment);
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath)); LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
} }
} }
@@ -137,325 +161,287 @@ void ClientInterface::sendMessage(
.arg(imageFiles.size())); .arg(imageFiles.size()));
} }
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments); if (!m_sessionManager) {
const QString error = QStringLiteral("Chat session manager is not available");
auto &chatAssistantSettings = Settings::chatAssistantSettings(); LOG_MESSAGE(error);
emit errorOccurred(error);
auto providerName = Settings::generalSettings().caProvider(); return;
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); }
if (!m_history) {
if (!provider) { const QString error = QStringLiteral("Chat history is not available");
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); LOG_MESSAGE(error);
emit errorOccurred(error);
return; return;
} }
auto templateName = Settings::generalSettings().caTemplate(); QString sessionError;
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); Session *session = m_sessionManager->createSession(m_activeAgent, m_history, &sessionError);
if (!session) {
if (!promptTemplate) { const QString error = sessionError.isEmpty()
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); ? QStringLiteral("No chat agent selected")
: sessionError;
LOG_MESSAGE(error);
emit errorOccurred(error);
return; return;
} }
PluginLLMCore::ContextData context; auto *client = session->client();
if (!client) {
const bool isToolsEnabled = useTools; const QString error = QStringLiteral("Chat agent has no live client");
LOG_MESSAGE(error);
if (chatAssistantSettings.useSystemPrompt()) { m_sessionManager->removeSession(session);
QString systemPrompt = chatAssistantSettings.systemPrompt(); emit errorOccurred(error);
return;
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
if (!lastRoleId.isEmpty()) {
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (!role.id.isEmpty())
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
}
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
systemPrompt += 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()) {
systemPrompt
+= QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %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 (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())
systemPrompt += QString("\n\n") + alwaysOnSkills;
const QString skillsCatalog = m_skillsManager->catalogText();
if (!skillsCatalog.isEmpty())
systemPrompt += 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;
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
.arg(invokedSkill->name, invokedSkill->body);
}
}
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
} }
const bool toolHistory = promptTemplate->supportsToolHistory(); auto *project = ProjectExplorer::ProjectManager::startupProject();
Templates::ContextRenderer::Bindings bindings;
bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString();
bindings.homeDir = QDir::homePath();
bindings.roleId = m_activeRoleId;
session->setContextBindings(bindings);
QVector<PluginLLMCore::Message> messages; const QString chatFilePath = m_chatFilePath;
int toolCallMsgIdx = -1; session->setContentLoader([chatFilePath](const QString &storedPath) {
for (const auto &msg : m_chatModel->getChatHistory()) { return ChatSerializer::loadContentFromStorage(chatFilePath, storedPath);
if (msg.role == ChatModel::ChatRole::Tool) { });
if (!toolHistory || msg.toolName.isEmpty()) {
continue;
}
if (toolCallMsgIdx < 0) { m_sessionManager->toolContributors().contribute(client->tools());
PluginLLMCore::Message assistantCall; client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
assistantCall.role = "assistant"; client->setTransferTimeout(
messages.append(assistantCall); static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
toolCallMsgIdx = messages.size() - 1;
}
PluginLLMCore::ToolCall call; const QString chatContext = buildChatContextLayer(message, linkedFiles);
call.id = msg.id; if (!chatContext.isEmpty())
call.name = msg.toolName; session->systemPrompt()->setLayer(QStringLiteral("chat.context"), chatContext);
call.arguments = msg.toolArguments;
messages[toolCallMsgIdx].toolCalls.append(call);
PluginLLMCore::Message toolResult; std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
toolResult.role = "tool"; blocks.push_back(std::make_unique<LLMQore::TextContent>(message));
toolResult.toolCallId = msg.id;
toolResult.toolName = msg.toolName;
toolResult.content = msg.toolResult;
messages.append(toolResult);
continue;
}
toolCallMsgIdx = -1; for (const auto &attachment : storedAttachments) {
blocks.push_back(
if (msg.role == ChatModel::ChatRole::FileEdit) { std::make_unique<StoredAttachmentContent>(attachment.filename, attachment.content));
continue;
}
PluginLLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
apiMessage.content += "\n\nAttached files:";
for (const auto &attachment : msg.attachments) {
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
if (!fileContent.isEmpty()) {
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
.arg(attachment.filename, decodedContent);
}
}
}
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) {
apiMessage.images = apiImages;
}
}
messages.append(apiMessage);
} }
if (!imageFiles.isEmpty() if (!storedImages.isEmpty() && session->supportsImages()) {
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) { for (const auto &image : storedImages) {
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored") blocks.push_back(std::make_unique<StoredImageContent>(
.arg(provider->name(), QString::number(imageFiles.size()))); image.fileName, image.storedPath, image.mediaType));
}
} else if (!storedImages.isEmpty() && !session->supportsImages()) {
LOG_MESSAGE(QString("Agent '%1' doesn't support images, %2 ignored")
.arg(m_activeAgent)
.arg(storedImages.size()));
} }
context.history = messages; if (!m_chatFilePath.isEmpty()) {
if (auto *todoTool
QJsonObject payload{ = qobject_cast<QodeAssist::Tools::TodoTool *>(client->tools()->tool("todo_tool"))) {
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
provider->prepareRequest(
payload,
promptTemplate,
context,
PluginLLMCore::RequestType::Chat,
useTools,
useThinking);
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
connect(
provider->client(),
&::LLMQore::BaseClient::chunkReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&ClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolResultReady,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider, !toolHistory};
emit requestStarted(requestId);
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->setCurrentSessionId(m_chatFilePath); todoTool->setCurrentSessionId(m_chatFilePath);
} }
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>( if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
provider->toolsManager()->tool("read_original_history"))) { client->tools()->tool("read_original_history"))) {
historyTool->setCurrentSessionId(m_chatFilePath); historyTool->setCurrentSessionId(m_chatFilePath);
} }
} }
connect(session, &Session::event, this, [this, session](const QodeAssist::ResponseEvent &ev) {
onSessionEvent(session, ev);
});
connect(
session, &Session::finished, this,
[this](const LLMQore::RequestID &id, const QString &) { onSessionFinished(id); });
connect(
session, &Session::failed, this,
[this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
onSessionFailed(id, error);
});
const LLMQore::RequestID requestId = session->send(std::move(blocks));
if (requestId.isEmpty()) {
const QString error = QStringLiteral("Failed to start chat request for agent '%1': %2")
.arg(m_activeAgent, session->lastError().message);
LOG_MESSAGE(error);
m_sessionManager->removeSession(session);
emit errorOccurred(error);
return;
}
m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
emit requestStarted(requestId);
}
QString ClientInterface::requestIdForSession(Session *session) const
{
for (auto it = m_activeRequests.cbegin(); it != m_activeRequests.cend(); ++it) {
if (it.value().session == session)
return it.key();
}
return {};
}
void ClientInterface::onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev)
{
if (ev.kind() != ResponseEvent::Kind::Usage)
return;
const auto *usage = ev.as<ResponseEvents::Usage>();
if (!usage)
return;
const QString requestId = requestIdForSession(session);
if (!requestId.isEmpty()) {
m_chatModel->setMessageUsage(
requestId,
usage->inputTokens,
usage->outputTokens,
usage->cachedTokens,
usage->reasoningTokens);
}
emit messageUsageReceived(
usage->inputTokens, usage->outputTokens, usage->cachedTokens, usage->reasoningTokens);
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(usage->inputTokens)
.arg(usage->outputTokens)
.arg(usage->cachedTokens)
.arg(usage->reasoningTokens));
}
void ClientInterface::onSessionFinished(const QString &requestId)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
QString applyError;
if (!Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError)) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
emit messageReceivedCompletely();
m_activeRequests.erase(it);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
}
void ClientInterface::onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
Session *session = it.value().session;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error.message));
emit errorOccurred(error.message);
m_activeRequests.erase(it);
if (session && m_sessionManager)
m_sessionManager->removeSession(session);
}
QString ClientInterface::buildChatContextLayer(
const QString &message, const QList<QString> &linkedFiles) const
{
QString context;
auto *project = ProjectExplorer::ProjectManager::startupProject();
if (project) {
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()
{ {
const auto providerName = Settings::generalSettings().caProvider(); if (m_history)
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); m_history->clear();
if (provider && !m_chatFilePath.isEmpty()
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
m_chatModel->clear();
} }
void ClientInterface::cancelRequest() void ClientInterface::cancelRequest()
{ {
QSet<PluginLLMCore::Provider *> providers; const auto requests = m_activeRequests;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear(); m_activeRequests.clear();
m_accumulatedResponses.clear();
m_awaitingContinuation.clear();
LOG_MESSAGE("All requests cancelled and state cleared"); for (auto it = requests.begin(); it != requests.end(); ++it) {
} Session *session = it.value().session;
if (session && m_sessionManager)
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request) m_sessionManager->removeSession(session);
{
const auto message = response.trimmed();
if (!message.isEmpty()) {
QString messageId = request["id"].toString();
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
} }
LOG_MESSAGE("All chat requests cancelled and state cleared");
} }
QString ClientInterface::getCurrentFileContext() const QString ClientInterface::getCurrentFileContext() const
@@ -482,166 +468,11 @@ QString ClientInterface::getCurrentFileContext() const
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content); return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
} }
QString ClientInterface::getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
}
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const Context::ContextManager *ClientInterface::contextManager() const
{ {
return m_contextManager; return m_contextManager;
} }
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value();
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
}
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
QString applyError;
bool applySuccess
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
if (!applySuccess) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
LOG_MESSAGE(
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
+ ": " + finalText);
emit messageReceivedCompletely();
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId))
return;
if (!info.usage)
return;
const auto &u = *info.usage;
m_chatModel->setMessageUsage(
requestId, u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
emit messageUsageReceived(
u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
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,
const QJsonObject &arguments)
{
const auto requestIt = m_activeRequests.constFind(requestId);
if (requestIt == m_activeRequests.constEnd()) {
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
return;
}
if (requestIt->dropPreToolText) {
m_chatModel->dropTrailingAssistantMessage(requestId);
}
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
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"};
@@ -689,46 +520,8 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
return imageData.toBase64(); return imageData.toBase64();
} }
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const
{
QVector<PluginLLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) {
QString base64Data
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
if (base64Data.isEmpty()) {
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
continue;
}
PluginLLMCore::ImageAttachment apiImage;
apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false;
apiImages.append(apiImage);
}
return apiImages;
}
void ClientInterface::setChatFilePath(const QString &filePath) void ClientInterface::setChatFilePath(const QString &filePath)
{ {
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
}
m_chatFilePath = filePath; m_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath); m_chatModel->setChatFilePath(filePath);
} }

View File

@@ -1,19 +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 <QObject> #include <QObject>
#include <QSet> #include <QPointer>
#include <QString> #include <QString>
#include <QVector>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Provider.hpp" #include <ErrorInfo.hpp>
#include "pluginllmcore/IPromptProvider.hpp"
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <ResponseEvent.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
namespace QodeAssist {
class SessionManager;
class Session;
class ConversationHistory;
}
namespace QodeAssist::Skills { namespace QodeAssist::Skills {
class SkillsManager; class SkillsManager;
} }
@@ -25,23 +31,24 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager); void 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();
Context::ContextManager *contextManager() const; Context::ContextManager *contextManager() const;
void setChatFilePath(const QString &filePath); void setChatFilePath(const QString &filePath);
QString chatFilePath() const; QString chatFilePath() const;
@@ -52,50 +59,35 @@ signals:
void messageUsageReceived( void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens); 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 handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
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,
const QJsonObject &arguments);
void handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput);
private: private:
void handleLLMResponse(const QString &response, const QJsonObject &request); void onSessionEvent(Session *session, const QodeAssist::ResponseEvent &ev);
void onSessionFinished(const QString &requestId);
void onSessionFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles( QString buildChatContextLayer(
const QString &basePrompt, const QList<QString> &linkedFiles) const; const QString &message, const QList<QString> &linkedFiles) const;
QString requestIdForSession(Session *session) const;
bool isImageFile(const QString &filePath) const; bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
bool dropPreToolText = false;
}; };
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; 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,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "FileEditController.hpp" #include "FileEditController.hpp"
@@ -9,15 +10,13 @@
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include "ChatModel.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "context/ChangesManager.h" #include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent) FileEditController::FileEditController(QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel)
{ {
auto &changes = Context::ChangesManager::instance(); auto &changes = Context::ChangesManager::instance();
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) { connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
@@ -79,7 +78,6 @@ void FileEditController::applyFileEdit(const QString &editId)
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId)); LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) { if (Context::ChangesManager::instance().applyFileEdit(editId)) {
emit infoMessage(QString("File edit applied successfully")); emit infoMessage(QString("File edit applied successfully"));
updateFileEditStatus(editId, "applied");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -94,7 +92,6 @@ void FileEditController::rejectFileEdit(const QString &editId)
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId)); LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) { if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
emit infoMessage(QString("File edit rejected")); emit infoMessage(QString("File edit rejected"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -109,7 +106,6 @@ void FileEditController::undoFileEdit(const QString &editId)
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId)); LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) { if (Context::ChangesManager::instance().undoFileEdit(editId)) {
emit infoMessage(QString("File edit undone successfully")); emit infoMessage(QString("File edit undone successfully"));
updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
emit errorOccurred( emit errorOccurred(
@@ -162,44 +158,6 @@ void FileEditController::openFileEditInEditor(const QString &editId)
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath)); LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
} }
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
{
auto messages = m_chatModel->getChatHistory();
for (int i = 0; i < messages.size(); ++i) {
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
QString content = messages[i].content;
const QString marker = "QODEASSIST_FILE_EDIT:";
int markerPos = content.indexOf(marker);
QString jsonStr = content;
if (markerPos >= 0) {
jsonStr = content.mid(markerPos + marker.length());
}
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject obj = doc.object();
obj["status"] = status;
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (!edit.statusMessage.isEmpty()) {
obj["status_message"] = edit.statusMessage;
}
QString updatedContent = marker
+ QString::fromUtf8(
QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_chatModel->updateMessageContent(editId, updatedContent);
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
}
break;
}
}
updateStats();
}
void FileEditController::applyAllForCurrentMessage() void FileEditController::applyAllForCurrentMessage()
{ {
if (m_currentRequestId.isEmpty()) { if (m_currentRequestId.isEmpty()) {
@@ -222,13 +180,6 @@ void FileEditController::applyAllForCurrentMessage()
: QString("Failed to apply some file edits:\n%1").arg(errorMsg)); : QString("Failed to apply some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
updateStats(); updateStats();
} }
@@ -254,13 +205,6 @@ void FileEditController::undoAllForCurrentMessage()
: QString("Failed to undo some file edits:\n%1").arg(errorMsg)); : QString("Failed to undo some file edits:\n%1").arg(errorMsg));
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
updateStats(); updateStats();
} }

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -8,14 +9,12 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class FileEditController : public QObject class FileEditController : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr); explicit FileEditController(QObject *parent = nullptr);
void setCurrentRequestId(const QString &requestId); void setCurrentRequestId(const QString &requestId);
void clearCurrentRequestId(); void clearCurrentRequestId();
@@ -40,9 +39,6 @@ signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status);
ChatModel *m_chatModel;
QString m_currentRequestId; QString m_currentRequestId;
int m_totalEdits{0}; int m_totalEdits{0};
int m_appliedEdits{0}; int m_appliedEdits{0};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,30 +1,27 @@
// 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 "InputTokenCounter.hpp" #include "InputTokenCounter.hpp"
#include <algorithm> #include <algorithm>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <utils/aspects.h> #include <utils/aspects.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatModel.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
#include <ConversationHistory.hpp>
#include <Message.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
InputTokenCounter::InputTokenCounter( InputTokenCounter::InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent) ConversationHistory *history, Context::ContextManager *contextManager, QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel) , m_history(history)
, m_contextManager(contextManager) , m_contextManager(contextManager)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
@@ -41,12 +38,6 @@ InputTokenCounter::InputTokenCounter(
this, this,
&InputTokenCounter::recompute); &InputTokenCounter::recompute);
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
rewireToolsChangedConnection();
recompute();
});
rewireToolsChangedConnection();
recompute(); recompute();
} }
@@ -73,24 +64,6 @@ void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
recompute(); recompute();
} }
void InputTokenCounter::rewireToolsChangedConnection()
{
if (m_toolsChangedConn)
QObject::disconnect(m_toolsChangedConn);
m_toolsChangedConn = {};
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider)
return;
auto *tm = provider->toolsManager();
if (!tm)
return;
m_toolsChangedConn = connect(
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
}
void InputTokenCounter::recompute() void InputTokenCounter::recompute()
{ {
int inputTokens = m_messageTokens; int inputTokens = m_messageTokens;
@@ -129,24 +102,10 @@ void InputTokenCounter::recompute()
} }
} }
const auto &history = m_chatModel->getChatHistory(); if (m_history) {
for (const auto &message : history) { for (const auto &message : m_history->messages()) {
inputTokens += Context::TokenUtils::estimateTokens(message.content); inputTokens += Context::TokenUtils::estimateTokens(message.text());
inputTokens += 4; // + role inputTokens += 4; // + role
}
if (settings.enableChatTools()) {
const auto providerName = Settings::generalSettings().caProvider();
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
providerName)) {
if (auto *tm = provider->toolsManager()) {
const QJsonArray toolDefs = tm->getToolsDefinitions();
if (!toolDefs.isEmpty()) {
const QByteArray serialized
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
inputTokens += static_cast<int>(serialized.size() / 4);
}
}
} }
} }

View File

@@ -1,26 +1,31 @@
// 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 <QStringList> #include <QStringList>
namespace QodeAssist {
class ConversationHistory;
}
namespace QodeAssist::Context { namespace QodeAssist::Context {
class ContextManager; class ContextManager;
} }
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatModel;
class InputTokenCounter : public QObject class InputTokenCounter : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
InputTokenCounter( InputTokenCounter(
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr); ConversationHistory *history,
Context::ContextManager *contextManager,
QObject *parent = nullptr);
int inputTokens() const; int inputTokens() const;
@@ -36,11 +41,8 @@ signals:
void inputTokensChanged(); void inputTokensChanged();
private: private:
void rewireToolsChangedConnection(); ConversationHistory *m_history;
ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
QMetaObject::Connection m_toolsChangedConn;
QStringList m_attachments; QStringList m_attachments;
QStringList m_linkedFiles; QStringList m_linkedFiles;

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 943 B

View File

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

After

Width:  |  Height:  |  Size: 350 B

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -19,6 +20,9 @@ ChatRootView {
colorGroup: SystemPalette.Active colorGroup: SystemPalette.Active
} }
property bool hasActiveError: false
readonly property color errorColor: "#d32f2f"
palette { palette {
window: sysPalette.window window: sysPalette.window
windowText: sysPalette.windowText windowText: sysPalette.windowText
@@ -124,9 +128,6 @@ ChatRootView {
icon.source: (typeof _chatview !== 'undefined') icon.source: (typeof _chatview !== 'undefined')
? "qrc:/qt/qml/ChatView/icons/open-in-editor.svg" ? "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
: "qrc:/qt/qml/ChatView/icons/open-in-window.svg" : "qrc:/qt/qml/ChatView/icons/open-in-window.svg"
ToolTip.text: (typeof _chatview !== 'undefined')
? qsTr("Move this chat to an editor tab")
: qsTr("Move this chat to a separate window")
onClicked: { onClicked: {
if (typeof _chatview !== 'undefined') if (typeof _chatview !== 'undefined')
root.relocateToSplit() root.relocateToSplit()
@@ -134,61 +135,80 @@ ChatRootView {
root.relocateToWindow() root.relocateToWindow()
} }
} }
toolsButton { relocateTooltip.text: (typeof _chatview !== 'undefined')
checked: root.useTools ? qsTr("Move this chat to an editor tab")
onCheckedChanged: { : qsTr("Move this chat to a separate window")
root.useTools = toolsButton.checked
}
}
thinkingMode {
checked: root.useThinking
enabled: root.isThinkingSupport
onCheckedChanged: {
root.useThinking = thinkingMode.checked
}
}
settingsButton.onClicked: root.openSettings() settingsButton.onClicked: root.openSettings()
configSelector { agentSelector {
model: root.availableConfigurations model: root.availableChatAgents
displayText: root.currentConfiguration displayText: root.currentChatAgent
onActivated: function(index) { onActivated: function(index) {
if (index > 0) { root.currentChatAgent = root.availableChatAgents[index]
root.applyConfiguration(root.availableConfigurations[index])
}
} }
Component.onCompleted: root.loadAvailableChatAgents()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableConfigurations() root.loadAvailableChatAgents()
} }
} }
roleSelector { roleSelector {
model: root.availableAgentRoles model: root.availableRoles
displayText: root.currentAgentRole displayText: root.currentRole
onActivated: function(index) { onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index]) root.currentRole = root.availableRoles[index]
} }
Component.onCompleted: root.loadAvailableRoles()
popup.onAboutToShow: { popup.onAboutToShow: {
root.loadAvailableAgentRoles() root.loadAvailableRoles()
} }
} }
} }
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 2
MessageNavigator {
id: messageNavigator
Layout.preferredWidth: 16
Layout.fillHeight: true
Layout.topMargin: 4
Layout.bottomMargin: 4
chatModel: root.chatModel
onMessageClicked: function(messageIndex) {
chatListView.userScrolledUp = true
chatListView.positionViewAtIndex(messageIndex, ListView.Beginning)
}
}
ListView { ListView {
id: chatListView id: chatListView
property bool userScrolledUp: false property bool userScrolledUp: false
function syncNavigatorCurrent() {
const top = indexAt(10, contentY + 4)
messageNavigator.updateCurrentFromModelIndex(top)
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
leftMargin: 5 leftMargin: 3
model: root.chatModel model: root.chatModel
clip: true clip: true
spacing: 0 spacing: 0
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000 cacheBuffer: 2000
onContentYChanged: Qt.callLater(syncNavigatorCurrent)
onMovingChanged: { onMovingChanged: {
if (moving) { if (moving) {
userScrolledUp = !atYEnd userScrolledUp = !atYEnd
@@ -275,6 +295,7 @@ ChatRootView {
if (!userScrolledUp) { if (!userScrolledUp) {
root.scrollToBottom() root.scrollToBottom()
} }
Qt.callLater(syncNavigatorCurrent)
} }
onContentHeightChanged: { onContentHeightChanged: {
@@ -370,6 +391,7 @@ ChatRootView {
} }
} }
} }
}
ScrollView { ScrollView {
id: view id: view
@@ -381,11 +403,10 @@ ChatRootView {
QQC.TextArea { QQC.TextArea {
id: messageInput id: messageInput
placeholderText: Qt.platform.os === "osx" placeholderText: qsTr("Type your message here... (%1 to send)").arg(root.sendShortcutText)
? qsTr("Type your message here... (⌘+↩ to send)")
: qsTr("Type your message here... (Ctrl+Enter to send)")
placeholderTextColor: palette.mid placeholderTextColor: palette.mid
color: palette.text color: palette.text
wrapMode: TextArea.Wrap
background: Rectangle { background: Rectangle {
radius: 2 radius: 2
color: palette.base color: palette.base
@@ -464,6 +485,9 @@ ChatRootView {
skillCommandPopup.dismiss() skillCommandPopup.dismiss()
event.accepted = true event.accepted = true
} }
} else if (root.isSendShortcut(event.key, event.modifiers)) {
root.sendChatMessage()
event.accepted = true
} }
} }
@@ -556,13 +580,21 @@ ChatRootView {
Layout.preferredHeight: 40 Layout.preferredHeight: 40
isCompressing: root.isCompressing isCompressing: root.isCompressing
isProcessing: root.isRequestInProgress
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage() sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest() : root.cancelRequest()
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg" sendButton.icon.source: root.isRequestInProgress
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg" ? ""
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop") : (root.hasActiveError ? "qrc:/qt/qml/ChatView/icons/warning-icon.svg"
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return") : "qrc:/qt/qml/ChatView/icons/chat-icon.svg")
: qsTr("Stop") sendButton.text: root.isRequestInProgress ? qsTr("Stop") : qsTr("Send")
sendButton.accentColor: (root.hasActiveError && !root.isRequestInProgress)
? root.errorColor : "transparent"
sendButtonTooltip.text: root.isRequestInProgress
? qsTr("Stop")
: (root.hasActiveError
? root.lastErrorMessage
: qsTr("Send message to LLM %1").arg(root.sendShortcutText))
compressButton.onClicked: compressConfirmDialog.open() compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression() cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles { syncOpenFiles {
@@ -589,6 +621,27 @@ ChatRootView {
messageInput.forceActiveFocus() 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)
@@ -616,6 +669,7 @@ ChatRootView {
} }
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()
@@ -638,13 +692,122 @@ ChatRootView {
onAccepted: root.compressCurrentChat() onAccepted: root.compressCurrentChat()
} }
Toast { Rectangle {
id: errorToast id: errorBanner
z: 1000
color: Qt.rgba(0.8, 0.2, 0.2, 0.9) z: 1000
border.color: Qt.darker(infoToast.color, 1.3) visible: root.hasActiveError && root.lastErrorMessage.length > 0
toastTextColor: "#FFFFFF"
width: parent.width / 2
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 10
anchors.bottomMargin: bottomBar.height + 48
height: visible ? errorRow.implicitHeight + 12 : 0
color: Qt.rgba(0.83, 0.18, 0.18, 0.96)
radius: 6
border.color: Qt.darker(color, 1.3)
border.width: 1
RowLayout {
id: errorRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 6
spacing: 8
TextEdit {
Layout.fillWidth: true
text: root.lastErrorMessage
color: "#FFFFFF"
font.pixelSize: 12
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
selectionColor: Qt.darker(errorBanner.color, 1.3)
}
Rectangle {
id: copyErrorButton
property bool copied: false
Layout.alignment: Qt.AlignTop
implicitWidth: copyErrorLabel.implicitWidth + 18
implicitHeight: 22
radius: 4
color: copyErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28)
: Qt.rgba(1, 1, 1, 0.16)
border.color: Qt.rgba(1, 1, 1, 0.45)
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Text {
id: copyErrorLabel
anchors.centerIn: parent
text: copyErrorButton.copied ? qsTr("Copied") : qsTr("Copy")
color: "#FFFFFF"
font.pixelSize: 12
}
MouseArea {
id: copyErrorMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.copyToClipboard(root.lastErrorMessage)
copyErrorButton.copied = true
copyErrorResetTimer.restart()
}
}
Timer {
id: copyErrorResetTimer
interval: 1500
onTriggered: copyErrorButton.copied = false
}
}
Rectangle {
id: closeErrorButton
Layout.alignment: Qt.AlignTop
implicitWidth: 22
implicitHeight: 22
radius: 4
color: closeErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28) : "transparent"
border.color: Qt.rgba(1, 1, 1, 0.45)
border.width: closeErrorMouse.containsMouse ? 1 : 0
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: "✕"
color: "#FFFFFF"
font.pixelSize: 12
}
MouseArea {
id: closeErrorMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.hasActiveError = false
}
}
}
} }
Toast { Toast {
@@ -664,27 +827,14 @@ ChatRootView {
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 2 y: (parent.height - height) / 2
baseSystemPrompt: root.baseSystemPrompt
currentAgentRole: root.currentAgentRole
currentAgentRoleDescription: root.currentAgentRoleDescription
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
activeRules: root.activeRules
activeRulesCount: root.activeRulesCount
onOpenSettings: root.openSettings() onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
} }
Connections { Connections {
target: root target: root
function onLastErrorMessageChanged() { function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) { if (root.lastErrorMessage.length > 0) {
errorToast.show(root.lastErrorMessage) root.hasActiveError = true
} }
} }
function onLastInfoMessageChanged() { function onLastInfoMessageChanged() {

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
import QtQuick import QtQuick
import ChatView import ChatView
@@ -355,10 +356,9 @@ Rectangle {
smooth: true smooth: true
mipmap: true mipmap: true
BusyIndicator { QoABusyIndicator {
anchors.centerIn: parent anchors.centerIn: parent
running: imageDisplay.status === Image.Loading running: imageDisplay.status === Image.Loading
visible: running
} }
Text { Text {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +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
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
Flow { Flow {
id: root id: root
property alias attachedFilesModel: attachRepeater.model property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid property color accentColor: palette.mid
property string iconPath property string iconPath
@@ -21,10 +23,10 @@ Flow {
rightPadding: 5 rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0 topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0 bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater { Repeater {
id: attachRepeater id: attachRepeater
delegate: FileItem { delegate: FileItem {
id: fileItem id: fileItem
@@ -32,7 +34,7 @@ Flow {
required property string modelData required property string modelData
filePath: modelData filePath: modelData
height: 30 height: 30
width: contentRow.width + 10 width: contentRow.width + 10
@@ -52,7 +54,7 @@ Flow {
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => { onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
contextMenu.popup() contextMenu.open()
} else if (mouse.button === Qt.MiddleButton || } else if (mouse.button === Qt.MiddleButton ||
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) { (mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
root.removeFileFromListByIndex(fileItem.index) root.removeFileFromListByIndex(fileItem.index)
@@ -70,27 +72,27 @@ Flow {
} }
} }
Menu { Platform.Menu {
id: contextMenu id: contextMenu
MenuItem { Platform.MenuItem {
text: "Open in Qt Creator" text: qsTr("Open in Qt Creator")
onTriggered: fileItem.openFileInEditor() onTriggered: fileItem.openFileInEditor()
} }
MenuItem { Platform.MenuItem {
text: "Open in External Editor" text: qsTr("Open in External Editor")
onTriggered: fileItem.openFileInExternalEditor() onTriggered: fileItem.openFileInExternalEditor()
} }
MenuSeparator {} Platform.MenuSeparator {}
MenuItem { Platform.MenuItem {
text: "Remove" text: qsTr("Remove")
onTriggered: root.removeFileFromListByIndex(fileItem.index) onTriggered: root.removeFileFromListByIndex(fileItem.index)
} }
} }
Row { Row {
id: contentRow id: contentRow
@@ -107,31 +109,31 @@ Flow {
sourceSize.width: 8 sourceSize.width: 8
sourceSize.height: 15 sourceSize.height: 15
} }
Text { Text {
id: fileNameText id: fileNameText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: palette.buttonText color: palette.buttonText
text: { text: {
const parts = modelData.split('/'); const parts = modelData.split('/');
return parts[parts.length - 1]; return parts[parts.length - 1];
} }
} }
MouseArea { MouseArea {
id: closeButton id: closeButton
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width + 5 width: closeIcon.width + 5
height: closeButton.width + 5 height: closeButton.width + 5
onClicked: root.removeFileFromListByIndex(index) onClicked: root.removeFileFromListByIndex(index)
Image { Image {
id: closeIcon id: closeIcon
anchors.centerIn: parent anchors.centerIn: parent
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg" source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
: "qrc:/qt/qml/ChatView/icons/close-light.svg" : "qrc:/qt/qml/ChatView/icons/close-light.svg"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

21
LICENSE
View File

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

View File

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

View File

@@ -1,5 +1,6 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -7,12 +8,11 @@
#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,6 +22,14 @@ class QNetworkAccessManager;
namespace QodeAssist { namespace QodeAssist {
class AgentFactory;
class Session;
class SessionManager;
namespace Templates {
struct ContextData;
}
class LLMClientInterface : public LanguageClient::BaseClientInterface class LLMClientInterface : public LanguageClient::BaseClientInterface
{ {
Q_OBJECT Q_OBJECT
@@ -30,8 +38,8 @@ public:
LLMClientInterface( LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry, AgentFactory &agentFactory,
PluginLLMCore::IPromptProvider *promptProvider, SessionManager &sessionManager,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger); IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override; ~LLMClientInterface() override;
@@ -51,12 +59,6 @@ public:
protected: protected:
void startImpl() override; void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
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);
@@ -66,22 +68,26 @@ private:
void handleCancelRequest(); void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage); void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
void onCompletionFinished(const QString &requestId);
void onCompletionFailed(const QString &requestId, const QString &error);
void finishRequest(const QString &requestId);
QString requestIdForSession(Session *session) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
}; };
PluginLLMCore::ContextData prepareContext( Templates::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint( QString pickCompletionAgent(const QString &filePath) const;
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings; const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings; const Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; AgentFactory &m_agentFactory;
PluginLLMCore::IProviderRegistry &m_providerRegistry; SessionManager &m_sessionManager;
Context::IDocumentReader &m_documentReader; Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger; IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,43 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
#include <memory>
#include <LLMQore/BaseClient.hpp> #include <LLMQore/BaseClient.hpp>
#include <LLMQore/ContentBlocks.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/filepath.h>
#include <context/DocumentContextReader.hpp> #include <context/DocumentContextReader.hpp>
#include <pluginllmcore/ResponseCleaner.hpp>
#include <context/DocumentReaderQtCreator.hpp> #include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp> #include <context/Utils.hpp>
#include <pluginllmcore/PromptTemplateManager.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <pluginllmcore/RulesLoader.hpp>
#include <logger/Logger.hpp> #include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp> #include <sources/common/ResponseCleaner.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp> #include <settings/QuickRefactorSettings.hpp>
#include <settings/ToolsSettings.hpp> #include <settings/ToolsSettings.hpp>
#include "sources/common/ContextData.hpp"
#include <AgentFactory.hpp>
#include <AgentRouter.hpp>
#include <ConversationHistory.hpp>
#include <Session.hpp>
#include <SessionManager.hpp>
#include <SystemPromptBuilder.hpp>
#include "sources/settings/PipelinesConfig.hpp"
#include "tools/ToolsRegistration.hpp"
namespace QodeAssist { namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent) QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
@@ -33,6 +50,16 @@ QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
QuickRefactorHandler::~QuickRefactorHandler() {} QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::setSessionManager(SessionManager *sessionManager)
{
m_sessionManager = sessionManager;
}
void QuickRefactorHandler::setAgentFactory(AgentFactory *agentFactory)
{
m_agentFactory = agentFactory;
}
void QuickRefactorHandler::sendRefactorRequest( void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions) TextEditor::TextEditorWidget *editor, const QString &instructions)
{ {
@@ -87,102 +114,109 @@ void QuickRefactorHandler::sendRefactorRequest(
prepareAndSendRequest(editor, instructions, range); prepareAndSendRequest(editor, instructions, range);
} }
QString QuickRefactorHandler::pickRefactorAgent(const QString &filePath) const
{
const QStringList roster = Settings::PipelinesConfig::load().rosters.quickRefactor;
if (roster.isEmpty() || !m_agentFactory)
return {};
AgentRouter::Context ctx;
ctx.filePath = filePath;
if (auto *project = ProjectExplorer::ProjectManager::projectForFile(
Utils::FilePath::fromString(filePath)))
ctx.projectName = project->displayName();
return AgentRouter::pickAgent(roster, ctx, *m_agentFactory);
}
void QuickRefactorHandler::prepareAndSendRequest( void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range) const Utils::Text::Range &range)
{ {
auto &settings = Settings::generalSettings(); const auto emitError = [this, editor](const QString &error) {
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
const auto providerName = settings.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error); LOG_MESSAGE(error);
RefactorResult result; RefactorResult result;
result.success = false; result.success = false;
result.errorMessage = error; result.errorMessage = error;
result.editor = editor; result.editor = editor;
emit refactoringCompleted(result); emit refactoringCompleted(result);
};
if (!m_sessionManager) {
emitError(QStringLiteral("Quick refactor session manager is not available"));
return; return;
} }
const auto templateName = settings.qrTemplate(); const QString filePath = editor->textDocument()->filePath().toUrlishString();
auto promptTemplate = promptManager.getChatTemplateByName(templateName); const QString agentName = pickRefactorAgent(filePath);
if (agentName.isEmpty()) {
if (!promptTemplate) { emitError(QStringLiteral("No quick refactor agent matches: %1").arg(filePath));
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return; return;
} }
QJsonObject payload{ QString sessionError;
{"model", Settings::generalSettings().qrModel()}, {"stream", true}}; Session *session = m_sessionManager->acquire(agentName, &sessionError);
if (!session) {
emitError(sessionError.isEmpty() ? QStringLiteral("No quick refactor agent selected")
: sessionError);
return;
}
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions); auto *client = session->client();
if (!client) {
m_sessionManager->removeSession(session);
emitError(QStringLiteral("Quick refactor agent has no live client"));
return;
}
bool enableTools = Settings::quickRefactorSettings().useTools(); const bool enableTools = Settings::quickRefactorSettings().useTools();
bool enableThinking = Settings::quickRefactorSettings().useThinking(); if (enableTools) {
provider->prepareRequest( m_sessionManager->toolContributors().contribute(client->tools());
payload, client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations());
promptTemplate, }
context,
PluginLLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
provider->client()->setMaxToolContinuations( session->systemPrompt()->setLayer(
Settings::toolsSettings().maxToolContinuations()); QStringLiteral("refactor"), buildSystemPrompt(editor, range));
client->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_isRefactoringInProgress = true; m_isRefactoringInProgress = true;
connect( connect(
provider->client(), session, &Session::finished, this,
&::LLMQore::BaseClient::requestCompleted, [this](const LLMQore::RequestID &id, const QString &) { onRefactorFinished(id); });
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
connect( connect(
provider->client(), session, &Session::failed, this,
&::LLMQore::BaseClient::requestFinalized, [this](const LLMQore::RequestID &id, const QodeAssist::ErrorInfo &error) {
this, onRefactorFailed(id, error);
&QuickRefactorHandler::handleRequestFinalized, });
Qt::UniqueConnection);
connect( std::vector<std::unique_ptr<LLMQore::ContentBlock>> blocks;
provider->client(), const QString userMessage = instructions.isEmpty()
&::LLMQore::BaseClient::requestFailed, ? QStringLiteral("Refactor the code to improve its quality and maintainability.")
this, : instructions;
&QuickRefactorHandler::handleRequestFailed, blocks.push_back(std::make_unique<LLMQore::TextContent>(userMessage));
Qt::UniqueConnection);
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;
QJsonObject request{{"id", requestId}}; m_activeRequests[requestId] = {QJsonObject{{"id", requestId}}, session};
m_activeRequests[requestId] = {request, provider};
} }
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( QString QuickRefactorHandler::buildSystemPrompt(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range)
const Utils::Text::Range &range,
const QString &instructions)
{ {
PluginLLMCore::ContextData context; Q_UNUSED(range)
auto textDocument = editor->textDocument(); auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader; Context::DocumentReaderQtCreator documentReader;
@@ -190,7 +224,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
if (!documentInfo.document) { if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available"); LOG_MESSAGE("Error: Document is not available");
return context; return Settings::quickRefactorSettings().systemPrompt();
} }
QTextCursor cursor = editor->textCursor(); QTextCursor cursor = editor->textCursor();
@@ -266,17 +300,6 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt(); QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
project, PluginLLMCore::RulesContext::QuickRefactor);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for quick refactor");
}
}
systemPrompt += "\n\nFile information:"; systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType; systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath; systemPrompt += "\nFile path: " + documentInfo.filePath;
@@ -290,7 +313,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
"\n- Your output will completely replace the selected code" "\n- Your output will completely replace the selected code"
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position" : "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
"\n- Your output will be inserted at the cursor location"; "\n- Your output will be inserted at the cursor location";
systemPrompt += "\n\n## Formatting Rules:" systemPrompt += "\n\n## Formatting Rules:"
"\n- Output ONLY the code itself, without ANY explanations or descriptions" "\n- Output ONLY the code itself, without ANY explanations or descriptions"
"\n- Do NOT include markdown code blocks (no ```, no language tags)" "\n- Do NOT include markdown code blocks (no ```, no language tags)"
@@ -298,9 +321,9 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
"\n- Do NOT repeat existing code, be precise with context" "\n- Do NOT repeat existing code, be precise with context"
"\n- Do NOT send in answer <cursor> or </cursor> and other tags" "\n- Do NOT send in answer <cursor> or </cursor> and other tags"
"\n- The output must be ready to insert directly into the editor as-is"; "\n- The output must be ready to insert directly into the editor as-is";
systemPrompt += "\n\n## Indentation and Whitespace:"; systemPrompt += "\n\n## Indentation and Whitespace:";
if (cursor.hasSelection()) { if (cursor.hasSelection()) {
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart()); QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
int leadingSpaces = 0; int leadingSpaces = 0;
@@ -332,7 +355,7 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
.arg(leadingSpaces); .arg(leadingSpaces);
} }
} }
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code" systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
"\n- Maintain consistent indentation for nested blocks" "\n- Maintain consistent indentation for nested blocks"
"\n- Do NOT remove or reduce the base indentation level" "\n- Do NOT remove or reduce the base indentation level"
@@ -345,103 +368,92 @@ PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath}); systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
} }
context.systemPrompt = systemPrompt; return systemPrompt;
QVector<PluginLLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
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)
auto id = m_lastRequestId;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.key() == id) {
const RequestContext &ctx = it.value();
ctx.provider->cancelRequest(id);
m_activeRequests.erase(it);
break;
}
}
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
{
if (requestId == m_lastRequestId) {
m_activeRequests.remove(requestId);
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true);
}
}
void QuickRefactorHandler::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (requestId != m_lastRequestId || !info.usage)
return; return;
const auto &u = *info.usage; const auto id = m_lastRequestId;
LOG_MESSAGE( m_isRefactoringInProgress = false;
QString("Quick refactor usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5") m_lastRequestId.clear();
.arg(requestId)
.arg(u.promptTokens) auto it = m_activeRequests.find(id);
.arg(u.completionTokens) if (it != m_activeRequests.end()) {
.arg(u.cachedPromptTokens) Session *session = it.value().session;
.arg(u.reasoningTokens)); m_activeRequests.erase(it);
if (session && m_sessionManager)
m_sessionManager->release(session);
}
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
} }
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error) void QuickRefactorHandler::onRefactorFinished(const QString &requestId)
{ {
if (requestId == m_lastRequestId) { if (requestId != m_lastRequestId)
m_activeRequests.remove(requestId); return;
m_isRefactoringInProgress = false;
RefactorResult result; auto it = m_activeRequests.find(requestId);
result.success = false; Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
result.errorMessage = error; if (it != m_activeRequests.end())
result.editor = m_currentEditor; m_activeRequests.erase(it);
emit refactoringCompleted(result);
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(
const QString &requestId, const QodeAssist::ErrorInfo &error)
{
if (requestId != m_lastRequestId)
return;
auto it = m_activeRequests.find(requestId);
Session *session = (it != m_activeRequests.end()) ? it.value().session.data() : nullptr;
if (it != m_activeRequests.end())
m_activeRequests.erase(it);
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,22 +1,27 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QJsonObject> #include <QJsonObject>
#include <QObject> #include <QObject>
#include <QPointer>
#include <LLMQore/BaseClient.hpp> #include <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;
@@ -34,6 +39,9 @@ public:
explicit QuickRefactorHandler(QObject *parent = nullptr); explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override; ~QuickRefactorHandler() override;
void setSessionManager(SessionManager *sessionManager);
void setAgentFactory(AgentFactory *agentFactory);
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions); void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest(); void cancelRequest();
@@ -42,30 +50,26 @@ public:
signals: signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result); void refactoringCompleted(const QodeAssist::RefactorResult &result);
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private: private:
void prepareAndSendRequest( void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor, TextEditor::TextEditorWidget *editor,
const QString &instructions, const QString &instructions,
const Utils::Text::Range &range); const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); void onRefactorFinished(const QString &requestId);
PluginLLMCore::ContextData prepareContext( void onRefactorFailed(const QString &requestId, const QodeAssist::ErrorInfo &error);
TextEditor::TextEditorWidget *editor, QString buildSystemPrompt(
const Utils::Text::Range &range, TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range);
const QString &instructions); QString pickRefactorAgent(const QString &filePath) const;
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; QPointer<Session> session;
}; };
QPointer<SessionManager> m_sessionManager;
QPointer<AgentFactory> m_agentFactory;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor; TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange; Utils::Text::Range m_currentRange;

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), 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, Qwen, DeepSeek), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
⚠️ **Important Notice About Paid Providers** ⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services: > When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
@@ -39,7 +39,8 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge) - **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet) - **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
- **File Context** — attach, link, or auto-sync open editor files for richer prompts - **File Context** — attach, link, or auto-sync open editor files for richer prompts
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, any OpenAI-compatible endpoint - **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control - **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users! **Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
@@ -54,6 +55,11 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details> </details>
<details>
<summary>Chat View Mode: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/5914dd78-c8a4-4d35-889a-10ec493d4c4b" width="600" alt="QodeAssistChat2">
</details>
<details> <details>
<summary>Multiline Code completion: (click to expand)</summary> <summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@@ -86,7 +92,27 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
## Install plugin to QtCreator ## Install plugin to QtCreator
### Method 1: Using QodeAssistUpdater (Beta) ### Method 1: Using the Extension Registry (Recommended)
You can install and update QodeAssist directly from within Qt Creator by adding the QodeAssist registry as an external extension repository.
1. Open the Extensions page (`Qt Creator → Extensions`) and switch to the **Browser** tab
2. Enable **Use External Repository**
3. Next to **Repository URLs**, click **Add** and paste the registry archive URL matching your Qt Creator version:
- **Latest (QtC 19)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
- **QtC 19**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc19.tar.gz`
- **QtC 18**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc18.tar.gz`
<details>
<summary>Example of extension registry: (click to expand)</summary>
<img width="600" alt="RegistryExample" src="https://github.com/user-attachments/assets/8ab8cf10-72e7-4961-8c5a-21d530378a05">
</details>
4. Click **Apply** — QodeAssist will appear in the extensions list, where you can **Install** it
5. Updates can be installed from the same screen when a new version is published
> **Note:** This is an external repository not maintained by The Qt Company. By adding it you accept responsibility for managing the associated risks, as stated in the Extensions page.
### Method 2: Using QodeAssistUpdater (Beta)
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification. QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
@@ -114,7 +140,7 @@ Download pre-built binary from [QodeAssistUpdater releases](https://github.com/P
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater). For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
### Method 2: Manual Installation ### Method 3: Manual Installation
1. Install Latest Qt Creator 1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator 2. Download the QodeAssist plugin for your Qt Creator
@@ -144,6 +170,8 @@ The Quick Setup feature provides one-click configuration for popular cloud AI mo
- **OpenAI** (gpt-5.2-codex) - **OpenAI** (gpt-5.2-codex)
- **Mistral AI** (Codestral 2501) - **Mistral AI** (Codestral 2501)
- **Google AI** (Gemini 2.5 Flash) - **Google AI** (Gemini 2.5 Flash)
- **Qwen** (Qwen3.6 Plus, Qwen3.7 Max)
- **DeepSeek** (DeepSeek V4 Flash, DeepSeek V4 Pro)
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings 3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go! All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
@@ -164,6 +192,8 @@ For advanced users or local models, choose your preferred provider and follow th
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API - **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral** - **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
- **[Google AI](docs/google-ai-configuration.md)** — Gemini - **[Google AI](docs/google-ai-configuration.md)** — Gemini
- **Qwen (Alibaba)** — DashScope OpenAI-compatible Chat and Responses endpoints
- **DeepSeek** — `deepseek-chat` and `deepseek-reasoner` (reasoning shown as thinking)
- **OpenAI-compatible** — OpenRouter and any custom endpoint - **OpenAI-compatible** — OpenRouter and any custom endpoint
### Recommended Models for Best Experience ### Recommended Models for Best Experience
@@ -186,9 +216,9 @@ For optimal coding assistance, we recommend using these top-tier models:
### Additional Configuration ### Additional Configuration
- **[Creating and Extending Agents](docs/creating-agents.md)** - Add custom agents or override bundled ones with TOML profiles
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts - **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens - **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore` - **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
## Features ## Features
@@ -228,7 +258,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries - **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
- **[File Context](docs/file-context.md)** - Attach or link files for better context - **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional) - Automatic syncing with open editor files (optional)
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks - Extended thinking / reasoning mode - shows streamed chain-of-thought for reasoning models (Claude, Google, OpenAI Responses, and OpenAI-compatible endpoints returning `reasoning_content` such as DeepSeek, Qwen, LM Studio, OpenRouter)
### Quick Refactoring ### Quick Refactoring
- Inline code refactoring directly in the editor with AI assistance - Inline code refactoring directly in the editor with AI assistance
@@ -443,7 +473,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
- **Custom Instructions** provide reusable templates that can be augmented with specific details - **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled - **Tool Calling** is available for Chat and Quick Refactor when enabled
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details. See [Agent Roles Guide](docs/agent-roles.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
## QtCreator Version Compatibility ## QtCreator Version Compatibility
@@ -502,7 +532,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository. 1. **Report Issues**: If you encounter any bugs or have suggestions for improvements, please [open an issue](https://github.com/Palm1r/qodeassist/issues) on our GitHub repository.
2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. 2. **Contribute**: Feel free to submit pull requests with bug fixes or new features. The easiest contribution is an agent preset for a provider or model you use — it's a single TOML file, no C++ required; see [Contributing your agent](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers. 3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
@@ -550,6 +580,10 @@ cmake --build .
## For Contributors ## For Contributors
### Adding an agent preset
New provider/model presets are plain TOML — extend a provider base, register the file in `agents.qrc`, and the test suite validates it automatically. Step-by-step guide: [docs/creating-agents.md](docs/creating-agents.md#contributing-your-agent-to-qodeassist).
### Code Style ### Code Style
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc - **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
@@ -560,6 +594,45 @@ cmake --build .
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc). For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
## License
QodeAssist is licensed under the **GNU General Public License v3.0**
(see [`LICENSE`](LICENSE)), with **additional attribution terms under
GPLv3 Section 7(b)**.
You are free to use, modify, and redistribute QodeAssist under GPL-3.0,
but you **must preserve** the original author attribution, copyright
notices, and project identification — including in source file headers,
the plugin metadata (`QodeAssist.json.in`), and the About dialog or
equivalent user-facing identification. Modified versions must be clearly
marked as different from the original.
### Commercial licensing
QodeAssist is also available under a separate commercial license for use
in proprietary or closed-source products without GPL-3.0 obligations.
For commercial licensing inquiries, contact **palm1r-github-dev@pm.me**.
### Qt Creator components and attributions
QodeAssist is a plugin for Qt Creator and incorporates certain components
(plugin templates, API headers, and related boilerplate) originating from
Qt Creator, which are copyright (C) The Qt Company Ltd.
These components are provided by The Qt Company under the GNU General
Public License version 3, annotated with **The Qt Company GPL Exception
1.0**. This exception permits the development and distribution of Qt
Creator plugins under licenses of the plugin author's own choosing,
notwithstanding the GPL's general linking requirements. It is this
exception that allows QodeAssist to be offered under both GPL-3.0 and a
separate commercial license.
The original copyright and license notices of The Qt Company are
preserved in the relevant source files and must not be removed.
For Qt Creator's licensing terms, see
[LICENSE.GPL3-EXCEPT](https://github.com/qt-creator/qt-creator/blob/master/LICENSES/LICENSE.GPL3-EXCEPT).
![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,5 +1,6 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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