Compare commits

..

1 Commits

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

View File

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

View File

@@ -2,10 +2,6 @@ cmake_minimum_required(VERSION 3.16)
project(QodeAssist) project(QodeAssist)
option(QODEASSIST_EXPERIMENTAL
"Enable experimental features" OFF)
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
@@ -18,9 +14,7 @@ find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
find_package(GTest) find_package(GTest)
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
en cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk
)
# IDE_VERSION is defined by QtCreator package # IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION}) string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
@@ -40,10 +34,10 @@ add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH} -DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
) )
add_subdirectory(sources) add_subdirectory(sources/external/llmqore)
add_subdirectory(logger)
add_subdirectory(pluginllmcore) add_subdirectory(pluginllmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger)
add_subdirectory(UIControls) add_subdirectory(UIControls)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context) add_subdirectory(context)
@@ -70,9 +64,6 @@ add_qtc_plugin(QodeAssist
QtCreator::CPlusPlus QtCreator::CPlusPlus
LLMQore LLMQore
PluginLLMCore PluginLLMCore
ProvidersConfig
Agents
Skills
QodeAssistChatViewplugin QodeAssistChatViewplugin
SOURCES SOURCES
.github/workflows/build_cmake.yml .github/workflows/build_cmake.yml
@@ -90,6 +81,8 @@ add_qtc_plugin(QodeAssist
templates/OpenAI.hpp templates/OpenAI.hpp
templates/MistralAI.hpp templates/MistralAI.hpp
templates/StarCoder2Fim.hpp templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp
templates/Qwen25CoderFIM.hpp templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp templates/OpenAICompatible.hpp
templates/Llama3.hpp templates/Llama3.hpp
@@ -102,7 +95,6 @@ add_qtc_plugin(QodeAssist
templates/Qwen3CoderFIM.hpp templates/Qwen3CoderFIM.hpp
templates/OpenAIResponses.hpp templates/OpenAIResponses.hpp
providers/Providers.hpp providers/Providers.hpp
providers/ProviderUrlUtils.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
@@ -115,10 +107,16 @@ add_qtc_plugin(QodeAssist
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp providers/CodestralProvider.hpp providers/CodestralProvider.cpp
providers/OpenAIResponses/ModelRequest.hpp
providers/OpenAIResponses/ResponseObject.hpp
providers/OpenAIResponses/GetResponseRequest.hpp
providers/OpenAIResponses/DeleteResponseRequest.hpp
providers/OpenAIResponses/CancelResponseRequest.hpp
providers/OpenAIResponses/ListInputItemsRequest.hpp
providers/OpenAIResponses/InputTokensRequest.hpp
providers/OpenAIResponses/ItemTypesReference.hpp
providers/OpenAIResponsesRequestBuilder.hpp
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
providers/QwenProvider.hpp providers/QwenProvider.cpp
providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp
providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
@@ -127,9 +125,6 @@ add_qtc_plugin(QodeAssist
QodeAssistClient.hpp QodeAssistClient.cpp QodeAssistClient.hpp QodeAssistClient.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.cpp chat/ChatOutputPane.h chat/ChatOutputPane.cpp
chat/NavigationPanel.hpp chat/NavigationPanel.cpp chat/NavigationPanel.hpp chat/NavigationPanel.cpp
chat/ChatDocument.hpp chat/ChatDocument.cpp
chat/ChatEditor.hpp chat/ChatEditor.cpp
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
ConfigurationManager.hpp ConfigurationManager.cpp ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp
@@ -162,19 +157,12 @@ add_qtc_plugin(QodeAssist
tools/ReadFileTool.hpp tools/ReadFileTool.cpp tools/ReadFileTool.hpp tools/ReadFileTool.cpp
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
tools/TodoTool.hpp tools/TodoTool.cpp tools/TodoTool.hpp tools/TodoTool.cpp
tools/ReadOriginalHistoryTool.hpp tools/ReadOriginalHistoryTool.cpp
tools/SkillTool.hpp tools/SkillTool.cpp
mcp/McpServerManager.hpp mcp/McpServerManager.cpp mcp/McpServerManager.hpp mcp/McpServerManager.cpp
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
) )
if(QODEASSIST_EXPERIMENTAL)
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
endif()
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable find_program(QtCreatorExecutable
NAMES NAMES
@@ -196,5 +184,5 @@ endif()
qt_add_translations(TARGETS QodeAssist qt_add_translations(TARGETS QodeAssist
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
RESOURCE_PREFIX "/translations" RESOURCE_PREFIX "/translations"
LUPDATE_OPTIONS -no-obsolete -locations none LUPDATE_OPTIONS -no-obsolete
) )

View File

@@ -1,124 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "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,39 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <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

@@ -23,11 +23,9 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/FileMentionPopup.qml qml/controls/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml qml/controls/FileEditsActionBar.qml
qml/controls/ContextViewer.qml qml/controls/ContextViewer.qml
qml/controls/SkillCommandPopup.qml
qml/controls/Toast.qml qml/controls/Toast.qml
qml/controls/TopBar.qml qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml qml/controls/SplitDropZone.qml
qml/controls/MessageNavigator.qml
RESOURCES RESOURCES
icons/attach-file-light.svg icons/attach-file-light.svg
@@ -45,12 +43,9 @@ qt_add_qml_module(QodeAssistChatView
icons/window-unlock.svg icons/window-unlock.svg
icons/chat-icon.svg icons/chat-icon.svg
icons/chat-pause-icon.svg icons/chat-pause-icon.svg
icons/warning-icon.svg
icons/new-chat-icon.svg
icons/rules-icon.svg icons/rules-icon.svg
icons/context-icon.svg icons/context-icon.svg
icons/open-in-editor.svg icons/open-in-editor.svg
icons/open-in-window.svg
icons/apply-changes-button.svg icons/apply-changes-button.svg
icons/undo-changes-button.svg icons/undo-changes-button.svg
icons/reject-changes-button.svg icons/reject-changes-button.svg
@@ -60,7 +55,6 @@ qt_add_qml_module(QodeAssistChatView
icons/tools-icon-off.svg icons/tools-icon-off.svg
icons/settings-icon.svg icons/settings-icon.svg
icons/compress-icon.svg icons/compress-icon.svg
icons/open-in-code.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
@@ -75,13 +69,7 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp ChatCompressor.hpp ChatCompressor.cpp
AgentRoleController.hpp AgentRoleController.cpp
ChatConfigurationController.hpp ChatConfigurationController.cpp
FileEditController.hpp FileEditController.cpp
InputTokenCounter.hpp InputTokenCounter.cpp
ChatHistoryStore.hpp ChatHistoryStore.cpp
FileMentionItem.hpp FileMentionItem.cpp FileMentionItem.hpp FileMentionItem.cpp
SessionFileRegistry.hpp SessionFileRegistry.cpp
) )
target_link_libraries(QodeAssistChatView target_link_libraries(QodeAssistChatView
@@ -98,9 +86,8 @@ target_link_libraries(QodeAssistChatView
QodeAssistUIControlsplugin QodeAssistUIControlsplugin
QodeAssistLogger QodeAssistLogger
LLMQore LLMQore
Skills
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
) )

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatCompressor.hpp" #include "ChatCompressor.hpp"
@@ -76,8 +75,6 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint(); const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint(); : promptTemplate->endpoint();
m_provider->client()->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_currentRequestId = m_provider->sendRequest( m_currentRequestId = m_provider->sendRequest(
QUrl(Settings::generalSettings().caUrl()), payload, endpoint); QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId)); LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
@@ -231,8 +228,6 @@ bool ChatCompressor::createCompressedChatFile(
summaryMessage["images"] = QJsonArray(); summaryMessage["images"] = QJsonArray();
root["messages"] = QJsonArray{summaryMessage}; root["messages"] = QJsonArray{summaryMessage};
root["compressedFrom"] = sourcePath;
root["compressedAt"] = QDateTime::currentDateTime().toString(Qt::ISODate);
if (QFile::exists(destPath)) if (QFile::exists(destPath))
QFile::remove(destPath); QFile::remove(destPath);

View File

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

View File

@@ -1,100 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "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,36 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once
#include <QObject>
#include <QStringList>
namespace QodeAssist::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,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include <utils/aspects.h> #include <utils/aspects.h>
@@ -12,6 +11,7 @@
#include <QUrl> #include <QUrl>
#include <QtQml> #include <QtQml>
#include "ChatAssistantSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "context/ChangesManager.h" #include "context/ChangesManager.h"
@@ -20,6 +20,14 @@ namespace QodeAssist::Chat {
ChatModel::ChatModel(QObject *parent) ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
auto &settings = Settings::chatAssistantSettings();
connect(
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
connect(&Context::ChangesManager::instance(), connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied, &Context::ChangesManager::fileEditApplied,
this, this,
@@ -78,16 +86,6 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
case Roles::IsRedacted: { case Roles::IsRedacted: {
return message.isRedacted; return message.isRedacted;
} }
case Roles::PromptTokens:
return message.promptTokens;
case Roles::CompletionTokens:
return message.completionTokens;
case Roles::CachedPromptTokens:
return message.cachedPromptTokens;
case Roles::ReasoningTokens:
return message.reasoningTokens;
case Roles::TotalTokens:
return message.promptTokens + message.completionTokens;
case Roles::Images: { case Roles::Images: {
QVariantList imagesList; QVariantList imagesList;
for (const auto &image : message.images) { for (const auto &image : message.images) {
@@ -126,11 +124,6 @@ QHash<int, QByteArray> ChatModel::roleNames() const
roles[Roles::Attachments] = "attachments"; roles[Roles::Attachments] = "attachments";
roles[Roles::IsRedacted] = "isRedacted"; roles[Roles::IsRedacted] = "isRedacted";
roles[Roles::Images] = "images"; roles[Roles::Images] = "images";
roles[Roles::PromptTokens] = "promptTokens";
roles[Roles::CompletionTokens] = "completionTokens";
roles[Roles::CachedPromptTokens] = "cachedPromptTokens";
roles[Roles::ReasoningTokens] = "reasoningTokens";
roles[Roles::TotalTokens] = "totalTokens";
return roles; return roles;
} }
@@ -214,7 +207,6 @@ void ChatModel::clear()
m_messages.clear(); m_messages.clear();
endResetModel(); endResetModel();
emit modelReseted(); emit modelReseted();
emit sessionUsageChanged();
} }
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
@@ -318,6 +310,12 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
return messages; return messages;
} }
int ChatModel::tokensThreshold() const
{
auto &settings = Settings::chatAssistantSettings();
return settings.chatTokensThreshold();
}
QString ChatModel::lastMessageId() const QString ChatModel::lastMessageId() const
{ {
return !m_messages.isEmpty() ? m_messages.last().id : ""; return !m_messages.isEmpty() ? m_messages.last().id : "";
@@ -332,37 +330,11 @@ void ChatModel::resetModelTo(int index)
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1); beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
m_messages.remove(index, m_messages.size() - index); m_messages.remove(index, m_messages.size() - index);
endRemoveRows(); endRemoveRows();
emit sessionUsageChanged();
} }
} }
QVariantList ChatModel::userMessagePreviews(int maxLength) const
{
QVariantList result;
const int limit = maxLength > 4 ? maxLength : 80;
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].role != ChatRole::User)
continue;
QString preview = m_messages[i].content;
preview.replace(QLatin1Char('\n'), QLatin1Char(' '));
preview.replace(QLatin1Char('\r'), QLatin1Char(' '));
preview.replace(QLatin1Char('\t'), QLatin1Char(' '));
preview = preview.simplified();
if (preview.size() > limit)
preview = preview.left(limit - 1).trimmed() + QChar(0x2026);
QVariantMap entry;
entry[QStringLiteral("messageIndex")] = i;
entry[QStringLiteral("preview")] = preview;
result.append(entry);
}
return result;
}
void ChatModel::addToolExecutionStatus( void ChatModel::addToolExecutionStatus(
const QString &requestId, const QString &requestId, const QString &toolId, const QString &toolName)
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments)
{ {
QString content = toolName; QString content = toolName;
@@ -373,15 +345,11 @@ void ChatModel::addToolExecutionStatus(
&& m_messages.last().role == ChatRole::Tool) { && m_messages.last().role == ChatRole::Tool) {
Message &lastMessage = m_messages.last(); Message &lastMessage = m_messages.last();
lastMessage.content = content; lastMessage.content = content;
lastMessage.toolName = toolName;
lastMessage.toolArguments = toolArguments;
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1)); LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1)); emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else { } else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size()); beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{ChatRole::Tool, content, toolId}; Message newMessage{ChatRole::Tool, content, toolId};
newMessage.toolName = toolName;
newMessage.toolArguments = toolArguments;
m_messages.append(newMessage); m_messages.append(newMessage);
endInsertRows(); endInsertRows();
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2") LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
@@ -390,38 +358,6 @@ void ChatModel::addToolExecutionStatus(
} }
} }
void ChatModel::dropTrailingAssistantMessage(const QString &requestId)
{
if (m_messages.isEmpty())
return;
const Message &last = m_messages.last();
if (last.role != ChatRole::Assistant || last.id != requestId)
return;
const int idx = m_messages.size() - 1;
beginRemoveRows(QModelIndex(), idx, idx);
m_messages.removeLast();
endRemoveRows();
LOG_MESSAGE(QString("Dropped leaked pre-tool assistant message at index %1").arg(idx));
}
void ChatModel::setToolMessageData(
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments,
const QString &toolResult)
{
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].role == ChatRole::Tool && m_messages[i].id == toolId) {
m_messages[i].toolName = toolName;
m_messages[i].toolArguments = toolArguments;
m_messages[i].toolResult = toolResult;
return;
}
}
}
void ChatModel::updateToolResult( void ChatModel::updateToolResult(
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result) const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
{ {
@@ -441,8 +377,6 @@ void ChatModel::updateToolResult(
for (int i = m_messages.size() - 1; i >= 0; --i) { for (int i = m_messages.size() - 1; i >= 0; --i) {
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) { if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
m_messages[i].content = toolName + "\n" + result; m_messages[i].content = toolName + "\n" + result;
m_messages[i].toolName = toolName;
m_messages[i].toolResult = result;
emit dataChanged(index(i), index(i)); emit dataChanged(index(i), index(i));
toolMessageFound = true; toolMessageFound = true;
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i)); LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
@@ -573,62 +507,6 @@ void ChatModel::updateMessageContent(const QString &messageId, const QString &ne
} }
} }
void ChatModel::setMessageUsage(
const QString &messageId,
int promptTokens,
int completionTokens,
int cachedPromptTokens,
int reasoningTokens)
{
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].id != messageId)
continue;
m_messages[i].promptTokens = promptTokens;
m_messages[i].completionTokens = completionTokens;
m_messages[i].cachedPromptTokens = cachedPromptTokens;
m_messages[i].reasoningTokens = reasoningTokens;
emit dataChanged(
index(i),
index(i),
{Roles::PromptTokens,
Roles::CompletionTokens,
Roles::CachedPromptTokens,
Roles::ReasoningTokens,
Roles::TotalTokens});
emit sessionUsageChanged();
return;
}
}
int ChatModel::sessionPromptTokens() const
{
int total = 0;
for (const auto &m : m_messages)
total += m.promptTokens;
return total;
}
int ChatModel::sessionCompletionTokens() const
{
int total = 0;
for (const auto &m : m_messages)
total += m.completionTokens;
return total;
}
int ChatModel::sessionCachedPromptTokens() const
{
int total = 0;
for (const auto &m : m_messages)
total += m.cachedPromptTokens;
return total;
}
int ChatModel::sessionTotalTokens() const
{
return sessionPromptTokens() + sessionCompletionTokens();
}
void ChatModel::setLoadingFromHistory(bool loading) void ChatModel::setLoadingFromHistory(bool loading)
{ {
m_loadingFromHistory = loading; m_loadingFromHistory = loading;

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -9,7 +8,6 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject>
#include <QtQmlIntegration> #include <QtQmlIntegration>
#include "context/ContentFile.hpp" #include "context/ContentFile.hpp"
@@ -19,28 +17,14 @@ namespace QodeAssist::Chat {
class ChatModel : public QAbstractListModel class ChatModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL) Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking }; enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted, Images };
RoleType = Qt::UserRole,
Content,
Attachments,
IsRedacted,
Images,
PromptTokens,
CompletionTokens,
CachedPromptTokens,
ReasoningTokens,
TotalTokens
};
Q_ENUM(Roles) Q_ENUM(Roles)
struct ImageAttachment struct ImageAttachment
@@ -60,15 +44,6 @@ public:
QList<Context::ContentFile> attachments; QList<Context::ContentFile> attachments;
QList<ImageAttachment> images; 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);
@@ -91,23 +66,15 @@ public:
QVector<Message> getChatHistory() const; QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const; QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
int tokensThreshold() const;
QString currentModel() const; QString currentModel() const;
QString lastMessageId() 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( void addToolExecutionStatus(
const QString &requestId, const QString &requestId, const QString &toolId, const QString &toolName);
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( void updateToolResult(
const QString &requestId, const QString &requestId,
const QString &toolId, const QString &toolId,
@@ -118,18 +85,6 @@ public:
void addRedactedThinkingBlock(const QString &requestId, const QString &signature); void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent); void updateMessageContent(const QString &messageId, const QString &newContent);
void setMessageUsage(
const QString &messageId,
int promptTokens,
int completionTokens,
int cachedPromptTokens,
int reasoningTokens);
int sessionPromptTokens() const;
int sessionCompletionTokens() const;
int sessionCachedPromptTokens() const;
int sessionTotalTokens() const;
void setLoadingFromHistory(bool loading); void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const; bool isLoadingFromHistory() const;
@@ -137,8 +92,8 @@ public:
QString chatFilePath() const; QString chatFilePath() const;
signals: signals:
void tokensThresholdChanged();
void modelReseted(); void modelReseted();
void sessionUsageChanged();
private slots: private slots:
void onFileEditApplied(const QString &editId); void onFileEditApplied(const QString &editId);

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,8 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <QPointer>
#include <QQuickItem> #include <QQuickItem>
#include <QVariantList> #include <QVariantList>
@@ -14,19 +12,9 @@
#include "pluginllmcore/PromptProviderChat.hpp" #include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor; class ChatCompressor;
class AgentRoleController;
class ChatConfigurationController;
class FileEditController;
class InputTokenCounter;
class ChatHistoryStore;
class SessionFileRegistry;
class ChatRootView : public QQuickItem class ChatRootView : public QQuickItem
{ {
@@ -50,7 +38,6 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL) Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL) Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL) Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged 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)
@@ -65,14 +52,11 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL) Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL) Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
QML_ELEMENT QML_ELEMENT
public: public:
ChatRootView(QQuickItem *parent = nullptr); ChatRootView(QQuickItem *parent = nullptr);
~ChatRootView() override;
ChatModel *chatModel() const; ChatModel *chatModel() const;
QString currentTemplate() const; QString currentTemplate() const;
@@ -100,8 +84,6 @@ 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 openRulesFolder();
@@ -109,11 +91,6 @@ public:
Q_INVOKABLE void openFileInEditor(const QString &filePath); Q_INVOKABLE void openFileInEditor(const QString &filePath);
Q_INVOKABLE void relocateToSplit();
Q_INVOKABLE void relocateToWindow();
void consumePendingChatFile();
Q_INVOKABLE void updateInputTokensCount(); Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const; int inputTokensCount() const;
@@ -145,8 +122,6 @@ public:
Q_INVOKABLE QString getRuleContent(int index); Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules(); Q_INVOKABLE void refreshRules();
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
bool useTools() const; bool useTools() const;
void setUseTools(bool enabled); void setUseTools(bool enabled);
bool useThinking() const; bool useThinking() const;
@@ -189,13 +164,6 @@ public:
bool isCompressing() const; bool isCompressing() const;
bool isInEditor() const;
void setInEditor(bool value);
QString chatTitle() const;
Q_INVOKABLE void requestNewChat();
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message);
void copyToClipboard(const QString &text); void copyToClipboard(const QString &text);
@@ -222,7 +190,6 @@ signals:
void lastErrorMessageChanged(); void lastErrorMessageChanged();
void lastInfoMessageChanged(); void lastInfoMessageChanged();
void sendShortcutTextChanged();
void activeRulesChanged(); void activeRulesChanged();
void activeRulesCountChanged(); void activeRulesCountChanged();
@@ -242,34 +209,15 @@ signals:
void compressionCompleted(const QString &compressedChatPath); void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error); void compressionFailed(const QString &error);
void isInEditorChanged();
void chatTitleChanged();
void openFilesChanged(); void openFilesChanged();
void closeHostRequested();
private: private:
QString computeChatTitle() const; void updateFileEditStatus(const QString &editId, const QString &status);
void triggerOpenChatCommand(Utils::Id commandId); QString getChatsHistoryDir() const;
void handOffSession(); QString getSuggestedFileName() const;
bool deferSendForAutoCompress( QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
void dispatchSend(
const QString &message,
const QStringList &attachments,
const QStringList &linkedFiles,
bool useTools,
bool useThinking);
bool hasImageAttachments(const QStringList &attachments) const; bool hasImageAttachments(const QStringList &attachments) const;
SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const;
ChatModel *m_chatModel; ChatModel *m_chatModel;
PluginLLMCore::PromptProviderChat m_promptProvider; PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
@@ -278,36 +226,28 @@ private:
QString m_recentFilePath; QString m_recentFilePath;
QStringList m_attachmentFiles; QStringList m_attachmentFiles;
QStringList m_linkedFiles; QStringList m_linkedFiles;
int m_messageTokensCount{0};
struct PendingSend { int m_inputTokensCount{0};
QString message;
QStringList attachments;
QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false;
};
PendingSend m_pendingSend;
bool m_isSyncOpenFiles; bool m_isSyncOpenFiles;
bool m_isInEditor = false;
mutable QString m_cachedChatTitle;
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules; QVariantList m_activeRules;
QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0};
int m_currentMessageAppliedEdits{0};
int m_currentMessagePendingEdits{0};
int m_currentMessageRejectedEdits{0};
QString m_lastInfoMessage; QString m_lastInfoMessage;
QStringList m_availableConfigurations;
QString m_currentConfiguration;
QStringList m_availableAgentRoles;
QString m_currentAgentRole;
ChatCompressor *m_chatCompressor; ChatCompressor *m_chatCompressor;
AgentRoleController *m_agentRoleController;
ChatConfigurationController *m_configurationController;
FileEditController *m_fileEditController;
InputTokenCounter *m_tokenCounter;
ChatHistoryStore *m_historyStore;
mutable QPointer<SessionFileRegistry> m_sessionFileRegistry;
mutable bool m_sessionFileRegistryResolved = false;
mutable QPointer<Skills::SkillsManager> m_skillsManager;
mutable bool m_skillsManagerResolved = false;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "Logger.hpp" #include "Logger.hpp"
@@ -81,15 +80,6 @@ QJsonObject ChatSerializer::serializeMessage(
messageObj["signature"] = message.signature; 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()) { if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray; QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) { for (const auto &attachment : message.attachments) {
@@ -113,17 +103,6 @@ QJsonObject ChatSerializer::serializeMessage(
messageObj["images"] = imagesArray; 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; return messageObj;
} }
@@ -136,9 +115,6 @@ ChatModel::Message ChatSerializer::deserializeMessage(
message.id = json["id"].toString(); message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false); message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString(); message.signature = json["signature"].toString();
message.toolName = json["toolName"].toString();
message.toolArguments = json["toolArguments"].toObject();
message.toolResult = json["toolResult"].toString();
if (json.contains("attachments")) { if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray(); QJsonArray attachmentsArray = json["attachments"].toArray();
@@ -163,14 +139,6 @@ ChatModel::Message ChatSerializer::deserializeMessage(
} }
} }
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; return message;
} }
@@ -212,10 +180,6 @@ bool ChatSerializer::deserializeChat(
message.images, message.images,
message.isRedacted, message.isRedacted,
message.signature); message.signature);
if (message.role == ChatModel::ChatRole::Tool) {
model->setToolMessageData(
message.id, message.toolName, message.toolArguments, message.toolResult);
}
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3") LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
.arg(message.images.size()) .arg(message.images.size())
.arg(message.isRedacted) .arg(message.isRedacted)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
@@ -15,7 +14,6 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QMimeDatabase> #include <QMimeDatabase>
#include <QRegularExpression>
#include <QUuid> #include <QUuid>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
@@ -30,36 +28,27 @@
#include <LLMQore/ToolsManager.hpp> #include <LLMQore/ToolsManager.hpp>
#include "tools/ReadOriginalHistoryTool.hpp"
#include "tools/TodoTool.hpp" #include "tools/TodoTool.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "ProvidersManager.hpp" #include "ProvidersManager.hpp"
#include "SkillsSettings.hpp"
#include "ToolsSettings.hpp" #include "ToolsSettings.hpp"
#include <RulesLoader.hpp> #include <RulesLoader.hpp>
#include <context/ChangesManager.h> #include <context/ChangesManager.h>
#include <sources/skills/SkillsManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( ClientInterface::ClientInterface(
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent) ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent) : QObject(parent)
, m_promptProvider(promptProvider)
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, 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();
@@ -72,11 +61,6 @@ void ClientInterface::sendMessage(
bool useTools, bool useTools,
bool useThinking) bool useThinking)
{ {
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message");
return;
}
cancelRequest(); cancelRequest();
m_accumulatedResponses.clear(); m_accumulatedResponses.clear();
@@ -175,20 +159,13 @@ void ClientInterface::sendMessage(
auto project = PluginLLMCore::RulesLoader::getActiveProject(); auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) { if (project) {
systemPrompt += QString("\n# Active project: %1").arg(project->displayName()); systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
systemPrompt += QString( systemPrompt += QString("\n# Active Project path: %1")
"\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()); .arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) { if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) { if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt systemPrompt += QString("\n# Active Build directory: %1")
+= QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(buildConfig->buildDirectory().toUrlishString()); .arg(buildConfig->buildDirectory().toUrlishString());
} }
} }
@@ -203,85 +180,15 @@ void ClientInterface::sendMessage(
systemPrompt += QString("\n# No active project in IDE"); 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()) { if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles); systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
} }
context.systemPrompt = systemPrompt; context.systemPrompt = systemPrompt;
} }
const bool toolHistory = promptTemplate->supportsToolHistory();
QVector<PluginLLMCore::Message> messages; QVector<PluginLLMCore::Message> messages;
int toolCallMsgIdx = -1;
for (const auto &msg : m_chatModel->getChatHistory()) { for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool) { if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
if (!toolHistory || msg.toolName.isEmpty()) {
continue;
}
if (toolCallMsgIdx < 0) {
PluginLLMCore::Message assistantCall;
assistantCall.role = "assistant";
messages.append(assistantCall);
toolCallMsgIdx = messages.size() - 1;
}
PluginLLMCore::ToolCall call;
call.id = msg.id;
call.name = msg.toolName;
call.arguments = msg.toolArguments;
messages[toolCallMsgIdx].toolCalls.append(call);
PluginLLMCore::Message toolResult;
toolResult.role = "tool";
toolResult.toolCallId = msg.id;
toolResult.toolName = msg.toolName;
toolResult.content = msg.toolResult;
messages.append(toolResult);
continue;
}
toolCallMsgIdx = -1;
if (msg.role == ChatModel::ChatRole::FileEdit) {
continue; continue;
} }
@@ -338,9 +245,6 @@ void ClientInterface::sendMessage(
provider->client()->setMaxToolContinuations( provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations()); Settings::toolsSettings().maxToolContinuations());
provider->client()->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
connect( connect(
provider->client(), provider->client(),
&::LLMQore::BaseClient::chunkReceived, &::LLMQore::BaseClient::chunkReceived,
@@ -353,12 +257,6 @@ void ClientInterface::sendMessage(
this, this,
&ClientInterface::handleFullResponse, &ClientInterface::handleFullResponse,
Qt::UniqueConnection); Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&ClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect( connect(
provider->client(), provider->client(),
&::LLMQore::BaseClient::requestFailed, &::LLMQore::BaseClient::requestFailed,
@@ -391,7 +289,7 @@ void ClientInterface::sendMessage(
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint); = provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}}; QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider, !toolHistory}; m_activeRequests[requestId] = {request, provider};
emit requestStarted(requestId); emit requestStarted(requestId);
@@ -401,10 +299,6 @@ void ClientInterface::sendMessage(
provider->toolsManager()->tool("todo_tool"))) { provider->toolsManager()->tool("todo_tool"))) {
todoTool->setCurrentSessionId(m_chatFilePath); todoTool->setCurrentSessionId(m_chatFilePath);
} }
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
provider->toolsManager()->tool("read_original_history"))) {
historyTool->setCurrentSessionId(m_chatFilePath);
}
} }
} }
@@ -555,29 +449,6 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
m_awaitingContinuation.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) void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
@@ -614,21 +485,14 @@ void ClientInterface::handleThinkingBlockReceived(
} }
void ClientInterface::handleToolExecutionStarted( void ClientInterface::handleToolExecutionStarted(
const QString &requestId, const QString &requestId, const QString &toolId, const QString &toolName)
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments)
{ {
const auto requestIt = m_activeRequests.constFind(requestId); if (!m_activeRequests.contains(requestId)) {
if (requestIt == m_activeRequests.constEnd()) {
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId)); LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
return; return;
} }
if (requestIt->dropPreToolText) { m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
m_chatModel->dropTrailingAssistantMessage(requestId);
}
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
m_awaitingContinuation.insert(requestId); m_awaitingContinuation.insert(requestId);
} }

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
@@ -12,13 +11,8 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Provider.hpp" #include "Provider.hpp"
#include "pluginllmcore/IPromptProvider.hpp" #include "pluginllmcore/IPromptProvider.hpp"
#include <LLMQore/BaseClient.hpp>
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ClientInterface : public QObject class ClientInterface : public QObject
@@ -30,8 +24,6 @@ public:
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr); ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager);
void sendMessage( void sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments = {}, const QList<QString> &attachments = {},
@@ -50,21 +42,15 @@ signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely(); void messageReceivedCompletely();
void requestStarted(const QString &requestId); void requestStarted(const QString &requestId);
void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
private slots: private slots:
void handlePartialResponse(const QString &requestId, const QString &partialText); void handlePartialResponse(const QString &requestId, const QString &partialText);
void handleFullResponse(const QString &requestId, const QString &fullText); void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error); void handleRequestFailed(const QString &requestId, const QString &error);
void handleThinkingBlockReceived( void handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature); const QString &requestId, const QString &thinking, const QString &signature);
void handleToolExecutionStarted( void handleToolExecutionStarted(
const QString &requestId, const QString &requestId, const QString &toolId, const QString &toolName);
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments);
void handleToolExecutionCompleted( void handleToolExecutionCompleted(
const QString &requestId, const QString &requestId,
const QString &toolId, const QString &toolId,
@@ -85,13 +71,11 @@ private:
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
PluginLLMCore::Provider *provider; PluginLLMCore::Provider *provider;
bool dropPreToolText = false;
}; };
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager; Context::ContextManager *m_contextManager;
Skills::SkillsManager *m_skillsManager = nullptr;
QString m_chatFilePath; QString m_chatFilePath;
QHash<QString, RequestContext> m_activeRequests; QHash<QString, RequestContext> m_activeRequests;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 526 B

View File

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

Before

Width:  |  Height:  |  Size: 943 B

View File

@@ -1,6 +1,17 @@
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg"> <svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round"> <g clip-path="url(#clip0_74_52)">
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="black"/> <mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="none"/> <path d="M44 0H0V44H44V0Z" fill="white"/>
</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>
</g>
<defs>
<clipPath id="clip0_74_52">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 943 B

View File

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

Before

Width:  |  Height:  |  Size: 348 B

View File

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

Before

Width:  |  Height:  |  Size: 350 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

21
LICENSE
View File

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

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
@@ -64,21 +63,6 @@ void LLMClientInterface::handleFullResponse(const QString &requestId, const QStr
m_performanceLogger.endTimeMeasurement(requestId); m_performanceLogger.endTimeMeasurement(requestId);
} }
void LLMClientInterface::handleRequestFinalized(
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) void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{ {
auto it = m_activeRequests.find(requestId); auto it = m_activeRequests.find(requestId);
@@ -341,12 +325,6 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
this, this,
&LLMClientInterface::handleFullResponse, &LLMClientInterface::handleFullResponse,
Qt::UniqueConnection); Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&LLMClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect( connect(
provider->client(), provider->client(),
&::LLMQore::BaseClient::requestFailed, &::LLMQore::BaseClient::requestFailed,
@@ -354,9 +332,6 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
&LLMClientInterface::handleRequestFailed, &LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection); Qt::UniqueConnection);
provider->client()->setTransferTimeout(
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
auto requestId auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active)); = provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider}; m_activeRequests[requestId] = {request, provider};

View File

@@ -1,10 +1,8 @@
// Copyright (C) 2024-2026 Petr Mironychev // Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once
#include <LLMQore/BaseClient.hpp>
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
@@ -54,8 +52,6 @@ protected:
private slots: private slots:
void handleFullResponse(const QString &requestId, const QString &fullText); 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 handleRequestFailed(const QString &requestId, const QString &error);
private: private:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#include "QuickRefactorHandler.hpp" #include "QuickRefactorHandler.hpp"
@@ -144,9 +143,6 @@ void QuickRefactorHandler::prepareAndSendRequest(
provider->client()->setMaxToolContinuations( provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations()); Settings::toolsSettings().maxToolContinuations());
provider->client()->setTransferTimeout(
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
m_isRefactoringInProgress = true; m_isRefactoringInProgress = true;
connect( connect(
@@ -156,13 +152,6 @@ void QuickRefactorHandler::prepareAndSendRequest(
&QuickRefactorHandler::handleFullResponse, &QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection); Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&QuickRefactorHandler::handleRequestFinalized,
Qt::UniqueConnection);
connect( connect(
provider->client(), provider->client(),
&::LLMQore::BaseClient::requestFailed, &::LLMQore::BaseClient::requestFailed,
@@ -389,26 +378,26 @@ void QuickRefactorHandler::handleLLMResponse(
void QuickRefactorHandler::cancelRequest() void QuickRefactorHandler::cancelRequest()
{ {
if (!m_isRefactoringInProgress) if (m_isRefactoringInProgress) {
return; auto id = m_lastRequestId;
const auto id = m_lastRequestId; for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
m_isRefactoringInProgress = false; if (it.key() == id) {
m_lastRequestId.clear(); const RequestContext &ctx = it.value();
ctx.provider->cancelRequest(id);
auto it = m_activeRequests.find(id);
if (it != m_activeRequests.end()) {
auto provider = it.value().provider;
m_activeRequests.erase(it); m_activeRequests.erase(it);
if (provider) break;
provider->cancelRequest(id);
} }
}
m_isRefactoringInProgress = false;
RefactorResult result; RefactorResult result;
result.success = false; result.success = false;
result.errorMessage = "Refactoring request was cancelled"; result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result); emit refactoringCompleted(result);
} }
}
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText) void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
{ {
@@ -419,22 +408,6 @@ void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QS
} }
} }
void QuickRefactorHandler::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (requestId != m_lastRequestId || !info.usage)
return;
const auto &u = *info.usage;
LOG_MESSAGE(
QString("Quick refactor 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 QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error) void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
{ {
if (requestId == m_lastRequestId) { if (requestId == m_lastRequestId) {

View File

@@ -1,13 +1,11 @@
// 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 <LLMQore/BaseClient.hpp>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <utils/textutils.h> #include <utils/textutils.h>
@@ -45,8 +43,6 @@ signals:
private slots: private slots:
void handleFullResponse(const QString &requestId, const QString &fullText); 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 handleRequestFailed(const QString &requestId, const QString &error);
private: private:

117
README.md
View File

@@ -6,7 +6,7 @@
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Discord](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf) [![Discord](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral, Qwen, DeepSeek), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet). ![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
⚠️ **Important Notice About Paid Providers** ⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services: > When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
@@ -35,12 +35,10 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking - **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library - **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs - **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
- **Agent Skills** — reusable folders of specialized instructions loaded on demand; discovered from `.qodeassist/skills/` and `.claude/skills/`, invoked automatically, with `/skill`, or always-on
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge) - **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet) - **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
- **File Context** — attach, link, or auto-sync open editor files for richer prompts - **File Context** — attach, link, or auto-sync open editor files for richer prompts
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint - **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, any OpenAI-compatible endpoint
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control - **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users! **Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
@@ -55,11 +53,6 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details> </details>
<details>
<summary>Chat View Mode: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/5914dd78-c8a4-4d35-889a-10ec493d4c4b" width="600" alt="QodeAssistChat2">
</details>
<details> <details>
<summary>Multiline Code completion: (click to expand)</summary> <summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@@ -92,27 +85,7 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
## Install plugin to QtCreator ## Install plugin to QtCreator
### Method 1: Using the Extension Registry (Recommended) ### Method 1: Using QodeAssistUpdater (Beta)
You can install and update QodeAssist directly from within Qt Creator by adding the QodeAssist registry as an external extension repository.
1. Open the Extensions page (`Qt Creator → Extensions`) and switch to the **Browser** tab
2. Enable **Use External Repository**
3. Next to **Repository URLs**, click **Add** and paste the registry archive URL matching your Qt Creator version:
- **Latest (QtC 19)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
- **QtC 19**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc19.tar.gz`
- **QtC 18**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc18.tar.gz`
<details>
<summary>Example of extension registry: (click to expand)</summary>
<img width="600" alt="RegistryExample" src="https://github.com/user-attachments/assets/8ab8cf10-72e7-4961-8c5a-21d530378a05">
</details>
4. Click **Apply** — QodeAssist will appear in the extensions list, where you can **Install** it
5. Updates can be installed from the same screen when a new version is published
> **Note:** This is an external repository not maintained by The Qt Company. By adding it you accept responsibility for managing the associated risks, as stated in the Extensions page.
### Method 2: Using QodeAssistUpdater (Beta)
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification. QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
@@ -140,7 +113,7 @@ Download pre-built binary from [QodeAssistUpdater releases](https://github.com/P
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater). For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
### Method 3: Manual Installation ### Method 2: Manual Installation
1. Install Latest Qt Creator 1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator 2. Download the QodeAssist plugin for your Qt Creator
@@ -170,8 +143,6 @@ The Quick Setup feature provides one-click configuration for popular cloud AI mo
- **OpenAI** (gpt-5.2-codex) - **OpenAI** (gpt-5.2-codex)
- **Mistral AI** (Codestral 2501) - **Mistral AI** (Codestral 2501)
- **Google AI** (Gemini 2.5 Flash) - **Google AI** (Gemini 2.5 Flash)
- **Qwen** (Qwen3.6 Plus, Qwen3.7 Max)
- **DeepSeek** (DeepSeek V4 Flash, DeepSeek V4 Pro)
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings 3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go! All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
@@ -192,8 +163,6 @@ For advanced users or local models, choose your preferred provider and follow th
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API - **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral** - **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
- **[Google AI](docs/google-ai-configuration.md)** — Gemini - **[Google AI](docs/google-ai-configuration.md)** — Gemini
- **Qwen (Alibaba)** — DashScope OpenAI-compatible Chat and Responses endpoints
- **DeepSeek** — `deepseek-chat` and `deepseek-reasoner` (reasoning shown as thinking)
- **OpenAI-compatible** — OpenRouter and any custom endpoint - **OpenAI-compatible** — OpenRouter and any custom endpoint
### Recommended Models for Best Experience ### Recommended Models for Best Experience
@@ -258,7 +227,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries - **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
- **[File Context](docs/file-context.md)** - Attach or link files for better context - **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional) - Automatic syncing with open editor files (optional)
- Extended thinking / reasoning mode - shows streamed chain-of-thought for reasoning models (Claude, Google, OpenAI Responses, and OpenAI-compatible endpoints returning `reasoning_content` such as DeepSeek, Qwen, LM Studio, OpenRouter) - Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
### Quick Refactoring ### Quick Refactoring
- Inline code refactoring directly in the editor with AI assistance - Inline code refactoring directly in the editor with AI assistance
@@ -284,41 +253,6 @@ Chat and Quick Refactor can call tools to inspect and modify your project. Each
| `execute_terminal_command` | Run a shell command (with confirmation) | | `execute_terminal_command` | Run a shell command (with confirmation) |
| `todo_tool` | Track multi-step task progress during a conversation | | `todo_tool` | Track multi-step task progress during a conversation |
### Skills
**Agent Skills** package specialized instructions and workflows into reusable folders the AI loads on demand. QodeAssist implements the open [Agent Skills](https://agentskills.io) format, so skills authored for Claude Code, Cursor, or other agents work as-is.
A skill is a folder containing a `SKILL.md` file — YAML frontmatter (`name`, `description`) plus Markdown instructions:
```
my-skill/
└── SKILL.md
```
```markdown
---
name: my-skill
description: What the skill does and when to use it.
---
# My Skill
Step-by-step instructions for the task...
```
**Where skills are discovered:**
- **Project skills** — project-relative subdirectories (default `.qodeassist/skills/` and `.claude/skills/`), configured in `Projects → QodeAssist → Skills`. Project skills win over global ones on a name collision.
- **Global skills** — absolute directories shared across all projects (default includes `~/.claude/skills/`), configured in `Tools → Options → QodeAssist → Skills`.
Both settings pages show the list of currently discovered skills.
**How skills are used in Chat:**
- **Automatically** — each skill's name and description is added to the system prompt; when a request matches, the model loads the full instructions via the `load_skill` tool (requires a tool-calling model).
- **Explicitly** — type `/` in the chat input and pick a skill from the popup; its instructions are injected into that one message. Works with any model.
- **Always-on** — a skill whose frontmatter has `metadata: always-on: "true"` is injected into every chat request automatically.
Enable or disable the whole feature in `Tools → Options → QodeAssist → Skills`.
### MCP Server ### MCP Server
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client. QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
@@ -520,7 +454,6 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
- [x] Quick refactoring with custom-instructions library - [x] Quick refactoring with custom-instructions library
- [x] Diff sharing with models - [x] Diff sharing with models
- [x] Tools / function calling (file I/O, build, terminal, diagnostics) - [x] Tools / function calling (file I/O, build, terminal, diagnostics)
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
- [x] Project-specific rules (`.qodeassist/rules/`) - [x] Project-specific rules (`.qodeassist/rules/`)
- [x] MCP (Model Context Protocol) — QodeAssist as a server - [x] MCP (Model Context Protocol) — QodeAssist as a server
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported) - [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
@@ -537,7 +470,6 @@ If you find QodeAssist helpful, there are several ways you can support the proje
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers. 3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following: 4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
- Paypal: [my paypalme page](https://www.paypal.com/paypalme/palm1r)
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t` - Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D` - Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy` - Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
@@ -590,45 +522,6 @@ 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,6 +1,5 @@
// Copyright (C) 2025-2026 Petr Mironychev // Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
#pragma once #pragma once

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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