Compare commits

..

61 Commits

Author SHA1 Message Date
Petr Mironychev
d180d189e4 chore: Update plugin to 0.9.17 version 2026-05-28 11:18:36 +02:00
Petr Mironychev
0774084ad9 fix: Add mouse propogation to qml chat in qquickwidget 2026-05-28 11:17:38 +02:00
Petr Mironychev
282f48d9fb fix: Improve parsing markdown 2026-05-28 10:50:25 +02:00
Petr Mironychev
8cbeb7132e fix: Replace context menu to system specific 2026-05-28 10:49:56 +02:00
Petr Mironychev
af898bd255 feat: Add preview to message navigator 2026-05-27 23:03:10 +02:00
Petr Mironychev
66e25300e8 tr: Update translations 2026-05-27 22:15:19 +02:00
Petr Mironychev
fcc651fd75 feat: Add message navigator 2026-05-27 22:06:41 +02:00
Petr Mironychev
dc016ce533 refactor: Improve file edit panel 2026-05-27 21:54:52 +02:00
Petr Mironychev
725de4a2c3 fix: Replace to custom tooltip 2026-05-27 19:23:28 +02:00
Petr Mironychev
8d3313d16b feat: Add translations to cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk 2026-05-27 18:42:56 +02:00
Petr Mironychev
abdcab3c7d fix: Add focus guard 2026-05-27 15:43:18 +02:00
Petr Mironychev
abadc2262c feat: Add session layout 2026-05-26 18:02:44 +02:00
Petr Mironychev
31ad99af61 refactor: Group sources cmake files to subfolder 2026-05-26 17:17:33 +02:00
Petr Mironychev
fb887967ed feat: Add agents pipelines 2026-05-26 16:44:45 +02:00
Petr Mironychev
97236c6069 feat: Add agents and agents settings 2026-05-26 12:31:00 +02:00
Petr Mironychev
51ebe3e523 Update README.md 2026-05-23 18:16:23 +02:00
Petr Mironychev
e193d1e1fa feat: Add settings page for providers (#353) 2026-05-21 19:30:32 +02:00
Petr Mironychev
ca3baa7597 chore: Update plugin to 0.9.16 2026-05-21 14:59:52 +02:00
Petr Mironychev
b33a1c2d43 fix: Add handling final argument for OpenAI responses tool calling 2026-05-21 14:19:16 +02:00
Petr Mironychev
c4e34bb3d9 chore: Update plugin to 0.9.15 version 2026-05-21 10:49:34 +02:00
Petr Mironychev
b9e0b5a00c fix: Change getting focus to chat in editor and name of title 2026-05-21 10:48:11 +02:00
Petr Mironychev
3f4bda51cd chore: Update plugin to 0.9.14 version 2026-05-19 09:48:42 +02:00
Petr Mironychev
7483c78777 feat: Add skills feature for tool and chat calling (#351) 2026-05-19 09:46:50 +02:00
Petr Mironychev
a3ad314cd4 feat: Add qt-docs mcp server 2026-05-17 21:40:48 +02:00
Petr Mironychev
74c899c8c3 fix: Add checking model support for tool calling (#350) 2026-05-17 21:27:18 +02:00
Petr Mironychev
6addcedfd0 feat: Add chat to editor view and refactor current openning 2026-05-15 23:44:43 +02:00
Petr Mironychev
eb7fc2f7b4 fix: Copy button can flow in expanded codeblock 2026-05-15 22:07:40 +02:00
Petr Mironychev
a06320d1c4 fix: LM Studio url and endpoints 2026-05-15 21:55:07 +02:00
Petr Mironychev
b1ca6823b8 fix: Throwing focus and hotkeys to QtCreator 2026-05-15 21:27:45 +02:00
Petr Mironychev
cc2d42f6d7 refactor: Move out InputTokenCounter, FileEditController, ChatHistoryStore, ChatConfigurationController 2026-05-15 20:17:22 +02:00
Petr Mironychev
4faeb90dc0 fix: Add missing files 2026-05-15 11:02:32 +02:00
Petr Mironychev
9f7497d15c refactor: Move out agent roles from Chat root view controller 2026-05-15 11:00:49 +02:00
Petr Mironychev
cab2f0a55e feat: Add too for reading original message history after compressing 2026-05-15 10:47:39 +02:00
Petr Mironychev
7704bffd88 feat: Add caching Claude provider 2026-05-15 10:28:21 +02:00
Petr Mironychev
3b421f60af refactor: Update token usage api (#347)
* refactor: Improve token usage api
* refactor: Image recognition to tokens
2026-05-14 21:19:12 +02:00
Petr Mironychev
86f4635080 feat: Add support QtCreator 19.0.2 (#346) 2026-05-14 09:33:49 +02:00
Petr Mironychev
f21757b9b3 Add paypal account 2026-05-12 15:15:09 +02:00
Ivan Lebedev
9bb6d55687 refactor: Uses single QML engine for all QML code (#323)
* refactor: Uses single QML engine for all QML code

* fix: Adds missing `QPointer` include

* Parents QML engine instance to plugin instance.

---------

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
Co-authored-by: Ivan Lebedev <ilebedev1988@gmail.com>
2026-04-28 16:33:03 +02:00
Petr Mironychev
bbb9c47cbb chore: Update plugin to 0.9.13 version 2026-04-27 07:12:52 +02:00
Petr Mironychev
46aa53e726 feat: Add llama.cpp modellisting and thinking support 2026-04-26 23:06:54 +02:00
Petr Mironychev
4d320bc065 refactor: Remove legacy code (#344) 2026-04-25 18:48:28 +02:00
Petr Mironychev
7b4e08859c feat: Add trying to parse ollama tools result when it's codeblock 2026-04-25 09:25:14 +02:00
Petr Mironychev
d15b46825e feat: Add support QtCreator 19.0.1 (#343) 2026-04-25 09:13:07 +02:00
Petr Mironychev
e0ab5080ea feat: Add llama.cpp api key support 2026-04-24 10:57:38 +02:00
Petr Mironychev
6a8fbe1792 chore: Update LLMQore to 0.4.2 2026-04-23 20:57:07 +02:00
Petr Mironychev
d867a6f0be docs: Add MCP client description 2026-04-23 20:13:32 +02:00
Petr Mironychev
248530c746 chore: Update plugin version to 0.9.12 2026-04-23 19:48:32 +02:00
Petr Mironychev
c73b71f328 feat: Add max continuation tools settings 2026-04-23 19:23:18 +02:00
Petr Mironychev
d2c1e39a2e chore: Update default ollama models 2026-04-23 11:29:07 +02:00
Petr Mironychev
e86e7e103e feat: Improve assemble string after code suggestion 2026-04-23 11:14:46 +02:00
Petr Mironychev
42199024ff refactor: Improvement code completion auto trigger 2026-04-23 10:56:23 +02:00
Petr Mironychev
620fded2e1 feat: Add mcp client hub 2026-04-23 10:18:57 +02:00
Petr Mironychev
90b7ed26b1 docs: Update current features in README.md 2026-04-23 03:42:35 +02:00
Petr Mironychev
25c4d5f185 feat: LM Studio response API and Ollama OpenAI API 2026-04-23 03:35:56 +02:00
Petr Mironychev
7a551ed384 feat: Add qodeassist mcp server 2026-04-23 02:40:46 +02:00
Petr Mironychev
ca0a47b160 feat: Improve Chat UI
Move send and compress button to right bottom corner
2026-04-23 01:48:17 +02:00
Petr Mironychev
6b069b55e3 chore: Update copyrights 2026-04-21 08:57:06 +02:00
Petr Mironychev
2891b313d2 refactor: Separate and simplified tools (#340) 2026-04-19 18:12:15 +02:00
Petr Mironychev
ede2c01eb7 Update LLMQore to v0.0.4 (#339) 2026-04-19 11:58:54 +02:00
Petr Mironychev
6c05f0d594 refactor: Add external LLMCore lib (#334)
* feat: Add LLMCore submodule
2026-04-03 12:30:40 +02:00
Ivan Lebedev
15d714588f fix: Open Qt Creator settings delayed from ChatRootView (#332)
fix: Opens settings delayed

Co-authored-by: Ivan Lebedev <ilebedev1988@gmail.com>
2026-03-29 11:58:34 +02:00
523 changed files with 68252 additions and 18198 deletions

View File

@@ -50,12 +50,14 @@ jobs:
qt_creator_version: "18.0.2"
}
- {
qt_version: "6.10.2",
qt_creator_version: "19.0.0"
qt_version: "6.10.3",
qt_creator_version: "19.0.2"
}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
with:
submodules: recursive
- name: Checkout submodules
id: git

2
.gitignore vendored
View File

@@ -78,3 +78,5 @@ CMakeLists.txt.user*
/.cursor
/.vscode
.qtc_clangd/compile_commands.json
CLAUDE.md
/.claude

3
.gitmodules vendored
View File

@@ -0,0 +1,3 @@
[submodule "sources/external/llmqore"]
path = sources/external/llmqore
url = https://github.com/Palm1r/llmqore.git

View File

@@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.16)
project(QodeAssist)
option(QODEASSIST_EXPERIMENTAL
"Enable experimental features" OFF)
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
@@ -14,7 +18,9 @@ find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
find_package(GTest)
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES
en cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk
)
# IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
@@ -34,9 +40,10 @@ add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
)
add_subdirectory(llmcore)
add_subdirectory(settings)
add_subdirectory(sources)
add_subdirectory(logger)
add_subdirectory(pluginllmcore)
add_subdirectory(settings)
add_subdirectory(UIControls)
add_subdirectory(ChatView)
add_subdirectory(context)
@@ -61,6 +68,11 @@ add_qtc_plugin(QodeAssist
QtCreator::ExtensionSystem
QtCreator::Utils
QtCreator::CPlusPlus
LLMQore
PluginLLMCore
ProvidersConfig
Agents
Skills
QodeAssistChatViewplugin
SOURCES
.github/workflows/build_cmake.yml
@@ -78,8 +90,6 @@ add_qtc_plugin(QodeAssist
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp
templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp
templates/Llama3.hpp
@@ -92,27 +102,20 @@ add_qtc_plugin(QodeAssist
templates/Qwen3CoderFIM.hpp
templates/OpenAIResponses.hpp
providers/Providers.hpp
providers/ProviderUrlUtils.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
providers/OpenAIResponses/ModelRequest.hpp
providers/OpenAIResponses/ResponseObject.hpp
providers/OpenAIResponses/GetResponseRequest.hpp
providers/OpenAIResponses/DeleteResponseRequest.hpp
providers/OpenAIResponses/CancelResponseRequest.hpp
providers/OpenAIResponses/ListInputItemsRequest.hpp
providers/OpenAIResponses/InputTokensRequest.hpp
providers/OpenAIResponses/ItemTypesReference.hpp
providers/OpenAIResponsesRequestBuilder.hpp
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
providers/OpenAIResponsesMessage.hpp providers/OpenAIResponsesMessage.cpp
QodeAssist.qrc
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
@@ -121,6 +124,9 @@ add_qtc_plugin(QodeAssist
QodeAssistClient.hpp QodeAssistClient.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.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
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
@@ -141,25 +147,31 @@ add_qtc_plugin(QodeAssist
widgets/DiffStatistics.hpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
tools/ToolHandler.hpp tools/ToolHandler.cpp
tools/ToolsRegistration.hpp tools/ToolsRegistration.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
tools/ToolsManager.hpp tools/ToolsManager.cpp
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
tools/EditFileTool.hpp tools/EditFileTool.cpp
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
tools/FindFileTool.hpp tools/FindFileTool.cpp
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
tools/TodoTool.hpp tools/TodoTool.cpp
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
tools/ReadOriginalHistoryTool.hpp tools/ReadOriginalHistoryTool.cpp
tools/SkillTool.hpp tools/SkillTool.cpp
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
mcp/McpClientsManager.hpp mcp/McpClientsManager.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)
find_program(QtCreatorExecutable
NAMES
@@ -181,5 +193,5 @@ endif()
qt_add_translations(TARGETS QodeAssist
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
RESOURCE_PREFIX "/translations"
LUPDATE_OPTIONS -no-obsolete
LUPDATE_OPTIONS -no-obsolete -locations none
)

View File

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

View File

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

View File

@@ -23,9 +23,11 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml
qml/controls/ContextViewer.qml
qml/controls/SkillCommandPopup.qml
qml/controls/Toast.qml
qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml
qml/controls/MessageNavigator.qml
RESOURCES
icons/attach-file-light.svg
@@ -43,9 +45,11 @@ qt_add_qml_module(QodeAssistChatView
icons/window-unlock.svg
icons/chat-icon.svg
icons/chat-pause-icon.svg
icons/new-chat-icon.svg
icons/rules-icon.svg
icons/context-icon.svg
icons/open-in-editor.svg
icons/open-in-window.svg
icons/apply-changes-button.svg
icons/undo-changes-button.svg
icons/reject-changes-button.svg
@@ -55,6 +59,7 @@ qt_add_qml_module(QodeAssistChatView
icons/tools-icon-off.svg
icons/settings-icon.svg
icons/compress-icon.svg
icons/open-in-code.svg
SOURCES
ChatWidget.hpp ChatWidget.cpp
@@ -69,7 +74,13 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.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
SessionFileRegistry.hpp SessionFileRegistry.cpp
)
target_link_libraries(QodeAssistChatView
@@ -80,13 +91,15 @@ target_link_libraries(QodeAssistChatView
Qt::Network
QtCreator::Core
QtCreator::Utils
LLMCore
PluginLLMCore
QodeAssistSettings
Context
QodeAssistUIControlsplugin
QodeAssistLogger
LLMQore
Skills
)
target_include_directories(QodeAssistChatView
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}
)

View File

@@ -1,23 +1,9 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatCompressor.hpp"
#include <LLMQore/BaseClient.hpp>
#include "ChatModel.hpp"
#include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
@@ -56,7 +42,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
}
auto providerName = Settings::generalSettings().caProvider();
m_provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
@@ -64,7 +50,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) {
@@ -76,30 +62,22 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
m_chatModel = chatModel;
m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear();
m_currentRequestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
emit compressionStarted();
connectProviderSignals();
QUrl requestUrl;
QJsonObject payload;
if (m_provider->providerID() == LLMCore::ProviderID::GoogleAI) {
requestUrl = QUrl(QString("%1/models/%2:streamGenerateContent?alt=sse")
.arg(Settings::generalSettings().caUrl(),
Settings::generalSettings().caModel()));
} else {
requestUrl = QUrl(QString("%1%2").arg(Settings::generalSettings().caUrl(),
m_provider->chatEndpoint()));
payload["model"] = Settings::generalSettings().caModel();
payload["stream"] = true;
}
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
buildRequestPayload(payload, promptTemplate);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
m_currentRequestId = m_provider->sendRequest(
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
m_provider->sendRequest(m_currentRequestId, requestUrl, payload);
}
bool ChatCompressor::isCompressing() const
@@ -188,28 +166,28 @@ QString ChatCompressor::buildCompressionPrompt() const
}
void ChatCompressor::buildRequestPayload(
QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate)
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
{
LLMCore::ContextData context;
PluginLLMCore::ContextData context;
context.systemPrompt = QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of discussion.");
QVector<LLMCore::Message> messages;
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool
|| msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
LLMCore::Message apiMessage;
PluginLLMCore::Message apiMessage;
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
apiMessage.content = msg.content;
messages.append(apiMessage);
}
LLMCore::Message compressionRequest;
PluginLLMCore::Message compressionRequest;
compressionRequest.role = "user";
compressionRequest.content = buildCompressionPrompt();
messages.append(compressionRequest);
@@ -217,7 +195,7 @@ void ChatCompressor::buildRequestPayload(
context.history = messages;
m_provider->prepareRequest(
payload, promptTemplate, context, LLMCore::RequestType::Chat, false, false);
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
}
bool ChatCompressor::createCompressedChatFile(
@@ -250,6 +228,8 @@ bool ChatCompressor::createCompressedChatFile(
summaryMessage["images"] = QJsonArray();
root["messages"] = QJsonArray{summaryMessage};
root["compressedFrom"] = sourcePath;
root["compressedAt"] = QDateTime::currentDateTime().toString(Qt::ISODate);
if (QFile::exists(destPath))
QFile::remove(destPath);
@@ -266,23 +246,25 @@ bool ChatCompressor::createCompressedChatFile(
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
m_provider,
&LLMCore::Provider::partialResponseReceived,
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
m_provider,
&LLMCore::Provider::fullResponseReceived,
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
m_provider,
&LLMCore::Provider::requestFailed,
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -24,10 +8,10 @@
#include <QObject>
#include <QString>
namespace QodeAssist::LLMCore {
namespace QodeAssist::PluginLLMCore {
class Provider;
class PromptTemplate;
} // namespace QodeAssist::LLMCore
} // namespace QodeAssist::PluginLLMCore
namespace QodeAssist::Chat {
@@ -64,13 +48,13 @@ private:
void disconnectAllSignals();
void cleanupState();
void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false;
QString m_currentRequestId;
QString m_originalChatPath;
QString m_accumulatedSummary;
LLMCore::Provider *m_provider = nullptr;
PluginLLMCore::Provider *m_provider = nullptr;
ChatModel *m_chatModel = nullptr;
QList<QMetaObject::Connection> m_connections;

View File

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

View File

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

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatFileManager.hpp"
#include "Logger.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -0,0 +1,228 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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

@@ -0,0 +1,47 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatModel.hpp"
#include <utils/aspects.h>
@@ -27,7 +11,6 @@
#include <QUrl>
#include <QtQml>
#include "ChatAssistantSettings.hpp"
#include "Logger.hpp"
#include "context/ChangesManager.h"
@@ -36,14 +19,6 @@ namespace QodeAssist::Chat {
ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent)
{
auto &settings = Settings::chatAssistantSettings();
connect(
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied,
this,
@@ -102,6 +77,16 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
case Roles::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: {
QVariantList imagesList;
for (const auto &image : message.images) {
@@ -140,6 +125,11 @@ QHash<int, QByteArray> ChatModel::roleNames() const
roles[Roles::Attachments] = "attachments";
roles[Roles::IsRedacted] = "isRedacted";
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;
}
@@ -223,6 +213,7 @@ void ChatModel::clear()
m_messages.clear();
endResetModel();
emit modelReseted();
emit sessionUsageChanged();
}
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
@@ -326,12 +317,6 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
return messages;
}
int ChatModel::tokensThreshold() const
{
auto &settings = Settings::chatAssistantSettings();
return settings.chatTokensThreshold();
}
QString ChatModel::lastMessageId() const
{
return !m_messages.isEmpty() ? m_messages.last().id : "";
@@ -346,11 +331,37 @@ void ChatModel::resetModelTo(int index)
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
m_messages.remove(index, m_messages.size() - index);
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(
const QString &requestId, const QString &toolId, const QString &toolName)
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments)
{
QString content = toolName;
@@ -361,11 +372,15 @@ void ChatModel::addToolExecutionStatus(
&& m_messages.last().role == ChatRole::Tool) {
Message &lastMessage = m_messages.last();
lastMessage.content = content;
lastMessage.toolName = toolName;
lastMessage.toolArguments = toolArguments;
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));
} else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{ChatRole::Tool, content, toolId};
newMessage.toolName = toolName;
newMessage.toolArguments = toolArguments;
m_messages.append(newMessage);
endInsertRows();
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
@@ -374,6 +389,38 @@ 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(
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
{
@@ -393,6 +440,8 @@ void ChatModel::updateToolResult(
for (int i = m_messages.size() - 1; i >= 0; --i) {
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
m_messages[i].content = toolName + "\n" + result;
m_messages[i].toolName = toolName;
m_messages[i].toolResult = result;
emit dataChanged(index(i), index(i));
toolMessageFound = true;
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
@@ -523,6 +572,62 @@ 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)
{
m_loadingFromHistory = loading;

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -24,6 +8,7 @@
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QtQmlIntegration>
#include "context/ContentFile.hpp"
@@ -33,14 +18,28 @@ namespace QodeAssist::Chat {
class ChatModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged 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
public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole)
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted, Images };
enum Roles {
RoleType = Qt::UserRole,
Content,
Attachments,
IsRedacted,
Images,
PromptTokens,
CompletionTokens,
CachedPromptTokens,
ReasoningTokens,
TotalTokens
};
Q_ENUM(Roles)
struct ImageAttachment
@@ -60,6 +59,15 @@ public:
QList<Context::ContentFile> attachments;
QList<ImageAttachment> images;
QString toolName;
QJsonObject toolArguments;
QString toolResult;
int promptTokens = 0;
int completionTokens = 0;
int cachedPromptTokens = 0;
int reasoningTokens = 0;
};
explicit ChatModel(QObject *parent = nullptr);
@@ -82,15 +90,23 @@ public:
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
int tokensThreshold() const;
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index);
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
void addToolExecutionStatus(
const QString &requestId, const QString &toolId, const QString &toolName);
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments);
void dropTrailingAssistantMessage(const QString &requestId);
void setToolMessageData(
const QString &toolId,
const QString &toolName,
const QJsonObject &toolArguments,
const QString &toolResult);
void updateToolResult(
const QString &requestId,
const QString &toolId,
@@ -100,6 +116,18 @@ public:
const QString &requestId, const QString &thinking, const QString &signature);
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent);
void setMessageUsage(
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);
bool isLoadingFromHistory() const;
@@ -108,8 +136,8 @@ public:
QString chatFilePath() const;
signals:
void tokensThresholdChanged();
void modelReseted();
void sessionUsageChanged();
private slots:
void onFileEditApplied(const QString &editId);

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,31 @@
/*
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QPointer>
#include <QQuickItem>
#include <QVariantList>
#include "ChatFileManager.hpp"
#include "ChatModel.hpp"
#include "ClientInterface.hpp"
#include "llmcore/PromptProviderChat.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat {
class ChatCompressor;
class AgentRoleController;
class ChatConfigurationController;
class FileEditController;
class InputTokenCounter;
class ChatHistoryStore;
class SessionFileRegistry;
class ChatRootView : public QQuickItem
{
@@ -68,11 +63,14 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
QML_ELEMENT
public:
ChatRootView(QQuickItem *parent = nullptr);
~ChatRootView() override;
ChatModel *chatModel() const;
QString currentTemplate() const;
@@ -107,6 +105,11 @@ public:
Q_INVOKABLE void openFileInEditor(const QString &filePath);
Q_INVOKABLE void relocateToSplit();
Q_INVOKABLE void relocateToWindow();
void consumePendingChatFile();
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
@@ -138,6 +141,8 @@ public:
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
bool useTools() const;
void setUseTools(bool enabled);
bool useThinking() const;
@@ -180,6 +185,13 @@ public:
bool isCompressing() const;
bool isInEditor() const;
void setInEditor(bool value);
QString chatTitle() const;
Q_INVOKABLE void requestNewChat();
public slots:
void sendMessage(const QString &message);
void copyToClipboard(const QString &text);
@@ -225,45 +237,72 @@ signals:
void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error);
void isInEditorChanged();
void chatTitleChanged();
void openFilesChanged();
void closeHostRequested();
private:
void updateFileEditStatus(const QString &editId, const QString &status);
QString getChatsHistoryDir() const;
QString getSuggestedFileName() const;
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
QString computeChatTitle() const;
void triggerOpenChatCommand(Utils::Id commandId);
void handOffSession();
bool deferSendForAutoCompress(
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;
SessionFileRegistry *sessionFileRegistry() const;
Skills::SkillsManager *skillsManager() const;
ChatModel *m_chatModel;
LLMCore::PromptProviderChat m_promptProvider;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath;
QStringList m_attachmentFiles;
QStringList m_linkedFiles;
int m_messageTokensCount{0};
int m_inputTokensCount{0};
struct PendingSend {
QString message;
QStringList attachments;
QStringList linkedFiles;
bool useTools = false;
bool useThinking = false;
bool active = false;
};
PendingSend m_pendingSend;
bool m_isSyncOpenFiles;
bool m_isInEditor = false;
mutable QString m_cachedChatTitle;
QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress;
QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0};
int m_currentMessageAppliedEdits{0};
int m_currentMessagePendingEdits{0};
int m_currentMessageRejectedEdits{0};
QString m_lastInfoMessage;
QStringList m_availableConfigurations;
QString m_currentConfiguration;
QStringList m_availableAgentRoles;
QString m_currentAgentRole;
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

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatSerializer.hpp"
#include "Logger.hpp"
@@ -96,6 +80,15 @@ QJsonObject ChatSerializer::serializeMessage(
messageObj["signature"] = message.signature;
}
if (message.role == ChatModel::ChatRole::Tool) {
if (!message.toolName.isEmpty())
messageObj["toolName"] = message.toolName;
if (!message.toolArguments.isEmpty())
messageObj["toolArguments"] = message.toolArguments;
if (!message.toolResult.isEmpty())
messageObj["toolResult"] = message.toolResult;
}
if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) {
@@ -119,6 +112,17 @@ QJsonObject ChatSerializer::serializeMessage(
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;
}
@@ -131,6 +135,9 @@ ChatModel::Message ChatSerializer::deserializeMessage(
message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
message.toolName = json["toolName"].toString();
message.toolArguments = json["toolArguments"].toObject();
message.toolResult = json["toolResult"].toString();
if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray();
@@ -155,6 +162,14 @@ 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;
}
@@ -196,6 +211,10 @@ bool ChatSerializer::deserializeChat(
message.images,
message.isRedacted,
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")
.arg(message.images.size())
.arg(message.isRedacted)

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatUtils.h"
@@ -35,22 +19,34 @@ QString ChatUtils::getSafeMarkdownText(const QString &text) const
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;
safeText.reserve(text.size());
safeText.reserve(text.size() + 16);
bool inFenced = false;
bool inInline = false;
for (int i = 0; i < text.size(); ++i) {
const QChar ch = text[i];
if (!inInline && i + 2 < text.size()
&& text[i] == '`' && text[i + 1] == '`' && text[i + 2] == '`') {
safeText.append(QStringLiteral("```"));
inFenced = !inFenced;
i += 2;
continue;
}
if (!inFenced && ch == '`') {
safeText.append(ch);
inInline = !inInline;
continue;
}
if (!inFenced && !inInline && ch == '<') {
safeText.append(QStringLiteral("&lt;"));
continue;
}
for (QChar ch : text) {
if (ch.isNull()) {
safeText.append(' ');
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,32 +1,24 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatView.hpp"
#include <QQmlComponent>
#include <QQmlContext>
#include <QQmlEngine>
#include <QQuickItem>
#include <QSettings>
#include <QVariantMap>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <logger/Logger.hpp>
#include "ChatRootView.hpp"
#include "QodeAssistConstants.hpp"
#include "SessionFileRegistry.hpp"
#include "sources/skills/SkillsManager.hpp"
namespace {
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
@@ -35,30 +27,65 @@ constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::Win
namespace QodeAssist::Chat {
ChatView::ChatView()
: m_isPin(false)
ChatView::ChatView(
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager)
: QQuickView{engine, nullptr}
, m_isPin(false)
{
setTitle("QodeAssist Chat");
engine()->rootContext()->setContextProperty("_chatview", this);
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
/// @note setup quick view content
{
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);
setMinimumSize({400, 300});
setFlags(baseFlags);
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
m_closeShortcut = new QShortcut(action->keySequence(), this);
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
if (m_closeShortcut) {
m_closeShortcut->setKey(action->keySequence());
}
});
}
bindCommandShortcut("QodeAssist.CloseChatView", [this] { close(); });
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE, [this] {
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
});
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_CLEAR_SESSION, [this] {
QMetaObject::invokeMethod(rootObject(), "clearChat");
});
restoreSettings();
}
void ChatView::bindCommandShortcut(Utils::Id commandId,
const std::function<void()> &onActivated)
{
auto command = Core::ActionManager::command(commandId);
if (!command)
return;
auto shortcut = new QShortcut(command->keySequence(), this);
connect(shortcut, &QShortcut::activated, this, onActivated);
connect(command, &Core::Command::keySequenceChanged, shortcut, [command, shortcut]() {
shortcut->setKey(command->keySequence());
});
}
void ChatView::closeEvent(QCloseEvent *event)
{
saveSettings();

View File

@@ -1,35 +1,32 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <functional>
#include <utils/id.h>
#include <QQuickView>
#include <QShortcut>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat {
class SessionFileRegistry;
class ChatView : public QQuickView
{
Q_OBJECT
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
public:
ChatView();
ChatView(
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager);
bool isPin() const;
void setIsPin(bool newIsPin);
@@ -43,9 +40,9 @@ protected:
private:
void saveSettings();
void restoreSettings();
void bindCommandShortcut(Utils::Id commandId, const std::function<void()> &onActivated);
bool m_isPin;
QShortcut *m_closeShortcut;
};
} // namespace QodeAssist::Chat

View File

@@ -1,34 +1,67 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatWidget.hpp"
#include <QApplication>
#include <QMouseEvent>
#include <QQmlContext>
#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 {
ChatWidget::ChatWidget(QWidget *parent)
: QQuickWidget(parent)
ChatWidget::ChatWidget(
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager,
bool registerOwnContext,
QWidget *parent)
: QQuickWidget{engine, parent}
{
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
/// @note setup quick view content
{
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);
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()
@@ -40,4 +73,35 @@ void ChatWidget::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

View File

@@ -1,41 +1,48 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QtQuickWidgets/QtQuickWidgets>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat {
class SessionFileRegistry;
class ChatWidget : public QQuickWidget
{
Q_OBJECT
public:
explicit ChatWidget(QWidget *parent = nullptr);
explicit ChatWidget(
QQmlEngine *engine,
SessionFileRegistry *sessionFileRegistry,
Skills::SkillsManager *skillsManager,
bool registerOwnContext = true,
QWidget *parent = nullptr);
~ChatWidget() = default;
Q_INVOKABLE void clear();
Q_INVOKABLE void scrollToBottom();
Q_INVOKABLE void focusInput();
void sendMessage();
void clearSession();
bool isChatFocused() const;
static ChatWidget *focusedInstance();
signals:
void clearPressed();
protected:
void focusInEvent(QFocusEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
};
} // namespace QodeAssist::Chat

View File

@@ -1,24 +1,10 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/target.h>
#include <texteditor/textdocument.h>
@@ -28,6 +14,7 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QUuid>
#include <coreplugin/editormanager/editormanager.h>
@@ -40,26 +27,38 @@
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <LLMQore/ToolsManager.hpp>
#include "tools/ReadOriginalHistoryTool.hpp"
#include "tools/TodoTool.hpp"
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "ProvidersManager.hpp"
#include "RequestConfig.hpp"
#include "SkillsSettings.hpp"
#include "ToolsSettings.hpp"
#include <RulesLoader.hpp>
#include <context/ChangesManager.h>
#include <sources/skills/SkillsManager.hpp>
namespace QodeAssist::Chat {
ClientInterface::ClientInterface(
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent)
, m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_chatModel(chatModel)
, m_contextManager(new Context::ContextManager(this))
{}
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
{
m_skillsManager = skillsManager;
}
ClientInterface::~ClientInterface()
{
cancelRequest();
@@ -72,6 +71,11 @@ void ClientInterface::sendMessage(
bool useTools,
bool useThinking)
{
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
LOG_MESSAGE("Ignoring empty chat message");
return;
}
cancelRequest();
m_accumulatedResponses.clear();
@@ -138,7 +142,7 @@ void ClientInterface::sendMessage(
auto &chatAssistantSettings = Settings::chatAssistantSettings();
auto providerName = Settings::generalSettings().caProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
@@ -153,7 +157,7 @@ void ClientInterface::sendMessage(
return;
}
LLMCore::ContextData context;
PluginLLMCore::ContextData context;
const bool isToolsEnabled = useTools;
@@ -167,22 +171,29 @@ void ClientInterface::sendMessage(
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
}
auto project = LLMCore::RulesLoader::getActiveProject();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
systemPrompt += QString("\n# Active Project path: %1")
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
systemPrompt += QString(
"\n# Project source root: %1"
"\n# All new source files, headers, QML and CMake edits MUST be "
"created or modified under this directory. Use absolute paths "
"rooted here, or project-relative paths.")
.arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt += QString("\n# Active Build directory: %1")
.arg(buildConfig->buildDirectory().toUrlishString());
systemPrompt
+= QString(
"\n# Build output directory (compiler artifacts only — do NOT "
"create or edit source files here): %1")
.arg(buildConfig->buildDirectory().toUrlishString());
}
}
QString projectRules
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
@@ -191,19 +202,89 @@ void ClientInterface::sendMessage(
systemPrompt += QString("\n# No active project in IDE");
}
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
projectSkillDirs = Settings::SkillsSettings::splitLines(
projectSettings.projectSkillDirs());
}
m_skillsManager->configure(
project ? project->projectDirectory().toFSPathString() : QString(),
Settings::SkillsSettings::splitPaths(
Settings::skillsSettings().globalSkillRoots()),
projectSkillDirs);
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
if (!alwaysOnSkills.isEmpty())
systemPrompt += QString("\n\n") + alwaysOnSkills;
const QString skillsCatalog = m_skillsManager->catalogText();
if (!skillsCatalog.isEmpty())
systemPrompt += QString("\n\n") + skillsCatalog;
static const QRegularExpression skillCommand(
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
QStringList invokedSkillNames;
auto skillMatch = skillCommand.globalMatch(message);
while (skillMatch.hasNext()) {
const QString skillName = skillMatch.next().captured(1);
if (invokedSkillNames.contains(skillName))
continue;
const auto invokedSkill = m_skillsManager->findByName(skillName);
if (invokedSkill && !invokedSkill->body.isEmpty()) {
invokedSkillNames << skillName;
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
.arg(invokedSkill->name, invokedSkill->body);
}
}
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
}
QVector<LLMCore::Message> messages;
const bool toolHistory = promptTemplate->supportsToolHistory();
QVector<PluginLLMCore::Message> messages;
int toolCallMsgIdx = -1;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
if (msg.role == ChatModel::ChatRole::Tool) {
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;
}
LLMCore::Message apiMessage;
toolCallMsgIdx = -1;
if (msg.role == ChatModel::ChatRole::FileEdit) {
continue;
}
PluginLLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
@@ -223,7 +304,8 @@ void ClientInterface::sendMessage(
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) {
apiMessage.images = apiImages;
@@ -233,112 +315,107 @@ void ClientInterface::sendMessage(
messages.append(apiMessage);
}
if (!imageFiles.isEmpty() && !provider->supportImage()) {
if (!imageFiles.isEmpty()
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size())));
}
context.history = messages;
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat;
config.provider = provider;
config.promptTemplate = promptTemplate;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3")
.arg(
Settings::generalSettings().caUrl(),
Settings::generalSettings().caModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
}
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
config.apiKey = provider->apiKey();
config.provider->prepareRequest(
config.providerRequest,
provider->prepareRequest(
payload,
promptTemplate,
context,
LLMCore::RequestType::Chat,
PluginLLMCore::RequestType::Chat,
useTools,
useThinking);
QString requestId = QUuid::createUuid().toString();
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider};
emit requestStarted(requestId);
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
connect(
provider,
&LLMCore::Provider::partialResponseReceived,
provider->client(),
&::LLMQore::BaseClient::chunkReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&ClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionStarted,
provider->client(),
&::LLMQore::BaseClient::toolStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionCompleted,
provider->client(),
&::LLMQore::BaseClient::toolResultReady,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::continuationStarted,
this,
&ClientInterface::handleCleanAccumulatedData,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::thinkingBlockReceived,
provider->client(),
&::LLMQore::BaseClient::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::redactedThinkingBlockReceived,
this,
&ClientInterface::handleRedactedThinkingBlockReceived,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider, !toolHistory};
emit requestStarted(requestId);
if (provider->supportsTools() && provider->toolsManager()) {
provider->toolsManager()->setCurrentSessionId(m_chatFilePath);
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->setCurrentSessionId(m_chatFilePath);
}
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
provider->toolsManager()->tool("read_original_history"))) {
historyTool->setCurrentSessionId(m_chatFilePath);
}
}
}
void ClientInterface::clearMessages()
{
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider && !m_chatFilePath.isEmpty() && provider->supportsTools()
if (provider && !m_chatFilePath.isEmpty()
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
provider->toolsManager()->clearTodoSession(m_chatFilePath);
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
m_chatModel->clear();
@@ -346,7 +423,7 @@ void ClientInterface::clearMessages()
void ClientInterface::cancelRequest()
{
QSet<LLMCore::Provider *> providers;
QSet<PluginLLMCore::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
@@ -354,7 +431,7 @@ void ClientInterface::cancelRequest()
}
for (auto *provider : providers) {
disconnect(provider, nullptr, this, nullptr);
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
@@ -366,6 +443,7 @@ void ClientInterface::cancelRequest()
m_activeRequests.clear();
m_accumulatedResponses.clear();
m_awaitingContinuation.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
@@ -432,6 +510,12 @@ void ClientInterface::handlePartialResponse(const QString &requestId, const QStr
if (it == m_activeRequests.end())
return;
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value();
@@ -462,12 +546,32 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
+ ": " + finalText);
emit messageReceivedCompletely();
if (it != m_activeRequests.end()) {
m_activeRequests.erase(it);
}
if (m_accumulatedResponses.contains(requestId)) {
m_accumulatedResponses.remove(requestId);
}
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
{
if (!m_activeRequests.contains(requestId))
return;
if (!info.usage)
return;
const auto &u = *info.usage;
m_chatModel->setMessageUsage(
requestId, u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
emit messageUsageReceived(
u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
.arg(requestId)
.arg(u.promptTokens)
.arg(u.completionTokens)
.arg(u.cachedPromptTokens)
.arg(u.reasoningTokens));
}
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
@@ -479,18 +583,9 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
emit errorOccurred(error);
if (it != m_activeRequests.end()) {
m_activeRequests.erase(it);
}
if (m_accumulatedResponses.contains(requestId)) {
m_accumulatedResponses.remove(requestId);
}
}
void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
{
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleThinkingBlockReceived(
@@ -501,30 +596,36 @@ void ClientInterface::handleThinkingBlockReceived(
return;
}
m_chatModel->addThinkingBlock(requestId, thinking, signature);
}
void ClientInterface::handleRedactedThinkingBlockReceived(
const QString &requestId, const QString &signature)
{
if (!m_activeRequests.contains(requestId)) {
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Ignoring redacted thinking block for non-chat request: %1").arg(requestId));
return;
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
m_chatModel->addRedactedThinkingBlock(requestId, signature);
if (thinking.isEmpty()) {
m_chatModel->addRedactedThinkingBlock(requestId, signature);
} else {
m_chatModel->addThinkingBlock(requestId, thinking, signature);
}
}
void ClientInterface::handleToolExecutionStarted(
const QString &requestId, const QString &toolId, const QString &toolName)
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments)
{
if (!m_activeRequests.contains(requestId)) {
const auto requestIt = m_activeRequests.constFind(requestId);
if (requestIt == m_activeRequests.constEnd()) {
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
return;
}
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
if (requestIt->dropPreToolText) {
m_chatModel->dropTrailingAssistantMessage(requestId);
}
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
m_awaitingContinuation.insert(requestId);
}
void ClientInterface::handleToolExecutionCompleted(
@@ -588,10 +689,10 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
return imageData.toBase64();
}
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const
{
QVector<LLMCore::ImageAttachment> apiImages;
QVector<PluginLLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) {
QString base64Data
@@ -601,7 +702,7 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
continue;
}
LLMCore::ImageAttachment apiImage;
PluginLLMCore::ImageAttachment apiImage;
apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false;
@@ -616,10 +717,15 @@ void ClientInterface::setChatFilePath(const QString &filePath)
{
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider && provider->supportsTools() && provider->toolsManager()) {
provider->toolsManager()->clearTodoSession(m_chatFilePath);
if (provider
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
}

View File

@@ -1,33 +1,23 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QSet>
#include <QString>
#include <QVector>
#include "ChatModel.hpp"
#include "Provider.hpp"
#include "llmcore/IPromptProvider.hpp"
#include "pluginllmcore/IPromptProvider.hpp"
#include <LLMQore/BaseClient.hpp>
#include <context/ContextManager.hpp>
namespace QodeAssist::Skills {
class SkillsManager;
}
namespace QodeAssist::Chat {
class ClientInterface : public QObject
@@ -36,9 +26,11 @@ class ClientInterface : public QObject
public:
explicit ClientInterface(
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface();
void setSkillsManager(Skills::SkillsManager *skillsManager);
void sendMessage(
const QString &message,
const QList<QString> &attachments = {},
@@ -57,17 +49,21 @@ signals:
void errorOccurred(const QString &error);
void messageReceivedCompletely();
void requestStarted(const QString &requestId);
void messageUsageReceived(
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
private slots:
void handlePartialResponse(const QString &requestId, const QString &partialText);
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
void handleCleanAccumulatedData(const QString &requestId);
void handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void handleRedactedThinkingBlockReceived(const QString &requestId, const QString &signature);
void handleToolExecutionStarted(
const QString &requestId, const QString &toolId, const QString &toolName);
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QJsonObject &arguments);
void handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
@@ -82,21 +78,24 @@ private:
bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const;
QVector<LLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
PluginLLMCore::Provider *provider;
bool dropPreToolText = false;
};
LLMCore::IPromptProvider *m_promptProvider = nullptr;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel;
Context::ContextManager *m_contextManager;
Skills::SkillsManager *m_skillsManager = nullptr;
QString m_chatFilePath;
QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
QSet<QString> m_awaitingContinuation;
};
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,334 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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

@@ -0,0 +1,53 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "FileItem.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "FileMentionItem.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -0,0 +1,183 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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

@@ -0,0 +1,53 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -0,0 +1,67 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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

@@ -0,0 +1,38 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="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>

After

Width:  |  Height:  |  Size: 526 B

View File

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

After

Width:  |  Height:  |  Size: 943 B

View File

@@ -1,17 +1,6 @@
<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 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="black"/>
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="none"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 943 B

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -0,0 +1,6 @@
<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>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
@@ -78,7 +62,22 @@ ChatRootView {
}
}
QoABusyOverlay {
id: compressingOverlay
z: 50
anchors.fill: mainColumn
anchors.topMargin: topBar.height
anchors.bottomMargin: bottomBar.height
active: root.isCompressing
text: qsTr("Compressing chat…")
}
ColumnLayout {
id: mainColumn
anchors.fill: parent
spacing: 0
@@ -88,14 +87,28 @@ ChatRootView {
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + 10
isCompressing: root.isCompressing
isInEditor: root.isInEditor
saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression()
newChatButton.onClicked: root.requestNewChat()
tokensBadge {
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
readonly property int sessionPrompt: root.chatModel.sessionPromptTokens || 0
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 {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
@@ -107,6 +120,20 @@ ChatRootView {
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
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 {
checked: root.useTools
onCheckedChanged: {
@@ -129,39 +156,67 @@ ChatRootView {
root.applyConfiguration(root.availableConfigurations[index])
}
}
popup.onAboutToShow: {
root.loadAvailableConfigurations()
}
}
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
}
}
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 2
MessageNavigator {
id: messageNavigator
Layout.preferredWidth: 16
Layout.fillHeight: true
Layout.topMargin: 4
Layout.bottomMargin: 4
chatModel: root.chatModel
onMessageClicked: function(messageIndex) {
chatListView.userScrolledUp = true
chatListView.positionViewAtIndex(messageIndex, ListView.Beginning)
}
}
ListView {
id: chatListView
property bool userScrolledUp: false
function syncNavigatorCurrent() {
const top = indexAt(10, contentY + 4)
messageNavigator.updateCurrentFromModelIndex(top)
}
Layout.fillWidth: true
Layout.fillHeight: true
leftMargin: 5
leftMargin: 3
model: root.chatModel
clip: true
spacing: 0
boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000
onContentYChanged: Qt.callLater(syncNavigatorCurrent)
onMovingChanged: {
if (moving) {
userScrolledUp = !atYEnd
@@ -248,6 +303,7 @@ ChatRootView {
if (!userScrolledUp) {
root.scrollToBottom()
}
Qt.callLater(syncNavigatorCurrent)
}
onContentHeightChanged: {
@@ -263,6 +319,7 @@ ChatRootView {
id: chatItemInstance
width: parent.width
chatViewport: chatListView
msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
messageImages: model.images
@@ -274,6 +331,10 @@ ChatRootView {
codeFontSize: root.codeFontSize
textFontSize: root.textFontSize
textFormat: root.textFormat
promptTokens: model.promptTokens || 0
completionTokens: model.completionTokens || 0
cachedPromptTokens: model.cachedPromptTokens || 0
reasoningTokens: model.reasoningTokens || 0
onResetChatToMessage: function(idx) {
messageInput.text = model.content
@@ -338,6 +399,7 @@ ChatRootView {
}
}
}
}
ScrollView {
id: view
@@ -376,15 +438,31 @@ ChatRootView {
root.calculateMessageTokensCount(messageInput.text)
var cursorPos = messageInput.cursorPosition
var textBefore = messageInput.text.substring(0, cursorPos)
var atIndex = textBefore.lastIndexOf('@')
if (atIndex >= 0) {
var query = textBefore.substring(atIndex + 1)
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
fileMentionPopup.updateSearch(query)
skillCommandPopup.dismiss()
return
}
}
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) {
@@ -402,6 +480,20 @@ ChatRootView {
fileMentionPopup.dismiss()
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
}
}
}
@@ -493,12 +585,16 @@ ChatRootView {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
isCompressing: root.isCompressing
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest()
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
: qsTr("Stop")
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
sendButtonTooltip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
: qsTr("Stop")
compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles {
checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
@@ -509,15 +605,6 @@ 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() {
root.clearMessages()
root.clearAttachmentFiles()
@@ -528,6 +615,31 @@ ChatRootView {
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() {
var result = fileMentionPopup.applyCurrentSelection(
messageInput.text, messageInput.cursorPosition, root.useTools)
@@ -537,6 +649,23 @@ 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() {
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = ""
@@ -636,7 +765,21 @@ 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: {
messageInput.forceActiveFocus()
focusInput()
}
}

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import ChatView
@@ -46,10 +30,16 @@ Rectangle {
property int textFontSize: Qt.application.font.pointSize
property int codeFontSize: Qt.application.font.pointSize
property int textFormat: 0
property Flickable chatViewport: null
property bool isUserMessage: false
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 openFileRequested(string filePath)
@@ -151,6 +141,39 @@ 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 {
@@ -237,6 +260,7 @@ Rectangle {
language: itemData.language
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
viewport: root.chatViewport
}
component AttachmentComponent : Rectangle {

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
@@ -29,6 +13,7 @@ Rectangle {
property string code: ""
property string language: ""
property bool expanded: false
property Flickable viewport: null
property alias codeFontFamily: codeText.font.family
property alias codeFontSize: codeText.font.pointSize
@@ -138,7 +123,16 @@ Rectangle {
anchors.right: parent.right
anchors.rightMargin: 5
y: 5
y: {
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")
onClicked: {

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
@@ -209,9 +193,24 @@ Rectangle {
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 {
id: headerText
Layout.fillWidth: true
text: {
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
if (root.oldContent.length > 0) {
@@ -239,6 +238,19 @@ Rectangle {
color: palette.mid
}
Item { Layout.fillWidth: true }
}
RowLayout {
id: actionButtons
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 6
Rectangle {
visible: !root.isPending
Layout.preferredWidth: badgeText.width + 12
@@ -255,31 +267,6 @@ Rectangle {
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 {
icon {

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import Qt.labs.platform as Platform

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import Qt.labs.platform as Platform

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import Qt.labs.platform as Platform

View File

@@ -1,31 +1,16 @@
/*
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.platform as Platform
import ChatView
import UIControls
Flow {
id: root
property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid
property string iconPath
@@ -37,10 +22,10 @@ Flow {
rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater {
id: attachRepeater
delegate: FileItem {
id: fileItem
@@ -48,7 +33,7 @@ Flow {
required property string modelData
filePath: modelData
height: 30
width: contentRow.width + 10
@@ -68,7 +53,7 @@ Flow {
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
contextMenu.popup()
contextMenu.open()
} else if (mouse.button === Qt.MiddleButton ||
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
root.removeFileFromListByIndex(fileItem.index)
@@ -86,27 +71,27 @@ Flow {
}
}
Menu {
Platform.Menu {
id: contextMenu
MenuItem {
text: "Open in Qt Creator"
Platform.MenuItem {
text: qsTr("Open in Qt Creator")
onTriggered: fileItem.openFileInEditor()
}
MenuItem {
text: "Open in External Editor"
Platform.MenuItem {
text: qsTr("Open in External Editor")
onTriggered: fileItem.openFileInExternalEditor()
}
MenuSeparator {}
Platform.MenuSeparator {}
MenuItem {
text: "Remove"
Platform.MenuItem {
text: qsTr("Remove")
onTriggered: root.removeFileFromListByIndex(fileItem.index)
}
}
Row {
id: contentRow
@@ -123,31 +108,31 @@ Flow {
sourceSize.width: 8
sourceSize.height: 15
}
Text {
id: fileNameText
anchors.verticalCenter: parent.verticalCenter
color: palette.buttonText
text: {
const parts = modelData.split('/');
return parts[parts.length - 1];
}
}
MouseArea {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width + 5
height: closeButton.width + 5
onClicked: root.removeFileFromListByIndex(index)
Image {
id: closeIcon
anchors.centerIn: parent
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
: "qrc:/qt/qml/ChatView/icons/close-light.svg"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
@@ -31,6 +15,11 @@ Rectangle {
property alias attachFiles: attachFilesId
property alias attachImages: attachImagesId
property alias linkFiles: linkFilesId
property alias compressButton: compressButtonId
property alias cancelCompressButton: cancelCompressButtonId
property bool isCompressing: false
property alias sendButtonTooltip: sendButtonTooltipId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
@@ -49,17 +38,6 @@ Rectangle {
spacing: 10
QoAButton {
id: sendButtonId
icon {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
}
QoAButton {
id: attachFilesId
@@ -68,9 +46,12 @@ Rectangle {
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Attach file to message")
QoAToolTip {
visible: attachFilesId.hovered
delay: 250
text: qsTr("Attach file to message")
}
}
QoAButton {
@@ -81,9 +62,12 @@ Rectangle {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Attach image to message")
QoAToolTip {
visible: attachImagesId.hovered
delay: 250
text: qsTr("Attach image to message")
}
}
QoAButton {
@@ -94,9 +78,12 @@ Rectangle {
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Link file to context")
QoAToolTip {
visible: linkFilesId.hovered
delay: 250
text: qsTr("Link file to context")
}
}
CheckBox {
@@ -104,12 +91,85 @@ Rectangle {
text: qsTr("Sync open files")
ToolTip.visible: syncOpenFilesId.hovered
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
QoAToolTip {
visible: syncOpenFilesId.hovered
text: qsTr("Automatically synchronize currently opened files with the model context")
}
}
Item {
Layout.fillWidth: true
}
Row {
id: compressingRow
visible: root.isCompressing
spacing: 6
BusyIndicator {
id: compressBusyIndicator
anchors.verticalCenter: parent.verticalCenter
running: root.isCompressing
width: 16
height: 16
}
Text {
text: qsTr("Compressing...")
anchors.verticalCenter: parent.verticalCenter
color: palette.text
font.pixelSize: 12
}
QoAButton {
id: cancelCompressButtonId
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Cancel")
QoAToolTip {
visible: cancelCompressButtonId.hovered
delay: 250
text: qsTr("Cancel compression")
}
}
}
QoAButton {
id: compressButtonId
visible: !root.isCompressing
text: qsTr("Compress")
icon {
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
height: 15
width: 15
}
QoAToolTip {
visible: compressButtonId.hovered
delay: 250
text: qsTr("Compress chat (create summarized copy using LLM)")
}
}
QoAButton {
id: sendButtonId
icon {
height: 15
width: 15
}
QoAToolTip {
id: sendButtonTooltipId
visible: sendButtonId.hovered
delay: 250
}
}
}
}

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
@@ -133,12 +117,14 @@ Rectangle {
text: root.hasPendingEdits
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: root.hasPendingEdits
? qsTr("Apply all pending and rejected edits in this message")
: qsTr("Reapply all rejected edits in this message")
QoAToolTip {
visible: applyAllButton.hovered
delay: 250
text: root.hasPendingEdits
? qsTr("Apply all pending and rejected edits in this message")
: qsTr("Reapply all rejected edits in this message")
}
onClicked: root.applyAllClicked()
}
@@ -149,10 +135,12 @@ Rectangle {
visible: root.hasAppliedEdits
enabled: root.hasAppliedEdits
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Undo all applied edits in this message")
QoAToolTip {
visible: undoAllButton.hovered
delay: 250
text: qsTr("Undo all applied edits in this message")
}
onClicked: root.undoAllClicked()
}

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls

View File

@@ -0,0 +1,187 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
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

@@ -0,0 +1,125 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
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,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Layouts
@@ -26,23 +10,24 @@ import UIControls
Rectangle {
id: root
property bool isInEditor: false
property alias saveButton: saveButtonId
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias compressButton: compressButtonId
property alias cancelCompressButton: cancelCompressButtonId
property alias newChatButton: newChatButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId
property alias relocateButton: relocateButtonId
property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId
property alias roleSelector: roleSelector
property bool isCompressing: false
property alias relocateTooltip: relocateTooltipId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
@@ -75,10 +60,76 @@ Rectangle {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
QoAToolTip {
visible: pinButtonId.hovered
delay: 250
text: pinButtonId.checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
}
QoAButton {
id: relocateButtonId
anchors.verticalCenter: parent.verticalCenter
icon {
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
id: relocateTooltipId
visible: relocateButtonId.hovered
delay: 250
}
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
QoAToolTip {
visible: clearButtonId.hovered
delay: 250
text: qsTr("Clean chat")
}
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
QoAButton {
id: newChatButtonId
visible: root.isInEditor
icon {
source: "qrc:/qt/qml/ChatView/icons/new-chat-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
QoAToolTip {
visible: newChatButtonId.hovered
delay: 250
text: qsTr("Open new chat in a new tab")
}
}
QoAComboBox {
@@ -89,9 +140,11 @@ Rectangle {
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch saved AI configuration")
QoAToolTip {
visible: configSelectorId.hovered
delay: 250
text: qsTr("Switch saved AI configuration")
}
}
QoAComboBox {
@@ -102,9 +155,11 @@ Rectangle {
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch agent role (different system prompts)")
QoAToolTip {
visible: roleSelector.hovered
delay: 250
text: qsTr("Switch agent role (different system prompts)")
}
}
}
@@ -127,15 +182,17 @@ Rectangle {
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
QoAToolTip {
visible: toolsButtonId.hovered
delay: 250
text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return toolsButtonId.checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
return checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
}
@@ -155,11 +212,14 @@ Rectangle {
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
QoAToolTip {
visible: thinkingModeId.hovered
delay: 250
text: thinkingModeId.enabled
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
}
QoAButton {
@@ -174,9 +234,11 @@ Rectangle {
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Open Chat Assistant Settings")
QoAToolTip {
visible: settingsButtonId.hovered
delay: 250
text: qsTr("Open Chat Assistant Settings")
}
}
QoASeparator {
@@ -202,9 +264,11 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
ToolTip.visible: containsMouse
ToolTip.delay: 500
ToolTip.text: recentPathId.text
QoAToolTip {
visible: parent.containsMouse && recentPathId.text.length > 0
text: recentPathId.text
delay: 500
}
}
}
}
@@ -225,9 +289,12 @@ Rectangle {
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Save chat to *.json file")
QoAToolTip {
visible: saveButtonId.hovered
delay: 250
text: qsTr("Save chat to *.json file")
}
}
QoAButton {
@@ -238,9 +305,12 @@ Rectangle {
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Load chat from *.json file")
QoAToolTip {
visible: loadButtonId.hovered
delay: 250
text: qsTr("Load chat from *.json file")
}
}
QoAButton {
@@ -251,62 +321,16 @@ Rectangle {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Show in system")
QoAToolTip {
visible: openChatHistoryId.hovered
delay: 250
text: qsTr("Show in system")
}
}
QoASeparator {}
QoAButton {
id: compressButtonId
visible: !root.isCompressing
icon {
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
}
Row {
id: compressingRow
visible: root.isCompressing
spacing: 6
BusyIndicator {
id: compressBusyIndicator
anchors.verticalCenter: parent.verticalCenter
running: root.isCompressing
width: 16
height: 16
}
Text {
text: qsTr("Compressing...")
height: parent.height
color: palette.text
font.pixelSize: 12
verticalAlignment: Text.AlignVCenter
}
QoAButton {
id: cancelCompressButtonId
text: qsTr("Cancel")
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Cancel compression")
}
}
QoAButton {
id: contextButtonId
@@ -317,9 +341,11 @@ Rectangle {
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
QoAToolTip {
visible: contextButtonId.hovered
delay: 250
text: qsTr("View chat context (system prompt, role, rules)")
}
}
Badge {
@@ -329,21 +355,6 @@ Rectangle {
ToolTip.delay: 250
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,22 +1,6 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ConfigurationManager.hpp"
@@ -41,7 +25,7 @@ void ConfigurationManager::init()
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
@@ -65,7 +49,7 @@ void ConfigurationManager::updateAllTemplateDescriptions()
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
@@ -86,8 +70,8 @@ void ConfigurationManager::checkAllTemplate()
ConfigurationManager::ConfigurationManager(QObject *parent)
: QObject(parent)
, m_generalSettings(Settings::generalSettings())
, m_providersManager(LLMCore::ProvidersManager::instance())
, m_templateManger(LLMCore::PromptTemplateManager::instance())
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
{}
void ConfigurationManager::setupConnections()
@@ -176,7 +160,7 @@ void ConfigurationManager::selectModel()
: m_generalSettings.caModel);
if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) {
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
return;
}

View File

@@ -1,28 +1,12 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include "llmcore/PromptTemplateManager.hpp"
#include "llmcore/ProvidersManager.hpp"
#include "pluginllmcore/PromptTemplateManager.hpp"
#include "pluginllmcore/ProvidersManager.hpp"
#include "settings/GeneralSettings.hpp"
namespace QodeAssist {
@@ -54,8 +38,8 @@ private:
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
Settings::GeneralSettings &m_generalSettings;
LLMCore::ProvidersManager &m_providersManager;
LLMCore::PromptTemplateManager &m_templateManger;
PluginLLMCore::ProvidersManager &m_providersManager;
PluginLLMCore::PromptTemplateManager &m_templateManger;
void setupConnections();
};

View File

@@ -1,24 +1,9 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "LLMClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
@@ -29,16 +14,15 @@
#include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include <llmcore/RequestConfig.hpp>
#include <llmcore/RulesLoader.hpp>
#include <pluginllmcore/RulesLoader.hpp>
namespace QodeAssist {
LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
PluginLLMCore::IProviderRegistry &providerRegistry,
PluginLLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
@@ -79,20 +63,36 @@ void LLMClientInterface::handleFullResponse(const QString &requestId, const QStr
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)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error;
@@ -122,8 +122,6 @@ void LLMClientInterface::sendData(const QByteArray &data)
} else if (method == "textDocument/didOpen") {
handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") {
QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId);
handleCompletion(request);
} else if (method == "$/cancelRequest") {
handleCancelRequest();
@@ -136,7 +134,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
void LLMClientInterface::handleCancelRequest()
{
QSet<LLMCore::Provider *> providers;
QSet<PluginLLMCore::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
@@ -144,7 +142,7 @@ void LLMClientInterface::handleCancelRequest()
}
for (auto *provider : providers) {
disconnect(provider, nullptr, this, nullptr);
disconnect(provider->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
@@ -270,39 +268,24 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
return;
}
// TODO refactor to dynamic presets system
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::CodeCompletion;
config.provider = provider;
config.promptTemplate = promptTemplate;
// TODO refactor networking
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
} else {
config.url = QUrl(
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
config.providerRequest = {{"model", modelName}, {"stream", true}};
}
config.apiKey = provider->apiKey();
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords;
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == LLMCore::TemplateType::Chat
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
auto project = LLMCore::RulesLoader::getActiveProject();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions);
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
@@ -314,10 +297,10 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
@@ -328,7 +311,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
@@ -338,50 +321,45 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
}
// TODO refactor add message
QVector<LLMCore::Message> messages;
QVector<PluginLLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
config.provider->prepareRequest(
config.providerRequest,
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion,
PluginLLMCore::RequestType::CodeCompletion,
false,
false);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
QString error = QString("Request validation failed: %1").arg(errors.join("; "));
LOG_MESSAGE("Validate errors for request:");
LOG_MESSAGES(errors);
sendErrorResponse(request, error);
return;
}
QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId);
m_activeRequests[requestId] = {request, provider};
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&LLMClientInterface::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_performanceLogger.startTimeMeasurement(requestId);
}
LLMCore::ContextData LLMClientInterface::prepareContext(
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
{
QJsonObject params = request["params"].toObject();
@@ -395,24 +373,12 @@ LLMCore::ContextData LLMClientInterface::prepareContext(
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
}
QString LLMClientInterface::endpoint(
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
QString LLMClientInterface::resolveEndpoint(
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
{
QString endpoint;
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
: m_generalSettings.ccEndpointMode.stringValue();
if (endpointMode == "Auto") {
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
: provider->chatEndpoint();
} else if (endpointMode == "Custom") {
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
} else if (endpointMode == "FIM") {
endpoint = provider->completionEndpoint();
} else if (endpointMode == "Chat") {
endpoint = provider->chatEndpoint();
}
return endpoint;
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
}
Context::ContextManager *LLMClientInterface::contextManager() const

View File

@@ -1,33 +1,18 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <LLMQore/BaseClient.hpp>
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
@@ -45,8 +30,8 @@ public:
LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
PluginLLMCore::IProviderRegistry &providerRegistry,
PluginLLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override;
@@ -68,6 +53,8 @@ protected:
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private:
@@ -82,17 +69,19 @@ private:
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
PluginLLMCore::Provider *provider;
};
LLMCore::ContextData prepareContext(
PluginLLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
QString resolveEndpoint(
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
LLMCore::IProviderRegistry &m_providerRegistry;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
PluginLLMCore::IProviderRegistry &m_providerRegistry;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer;

View File

@@ -1,26 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "LLMSuggestion.hpp"
#include <texteditor/texteditor.h>
@@ -29,56 +9,43 @@
namespace QodeAssist {
static QStringList extractTokens(const QString &str)
static bool isClosingTail(const QString &s, int from)
{
QStringList tokens;
QString currentToken;
for (const QChar &ch : str) {
if (ch.isLetterOrNumber() || ch == '_') {
currentToken += ch;
} else {
if (!currentToken.isEmpty() && currentToken.length() > 1) {
tokens.append(currentToken);
}
currentToken.clear();
}
static const QString closeChars = QStringLiteral("(){}[];,");
for (int i = from; i < s.size(); ++i) {
const QChar c = s.at(i);
if (!c.isSpace() && !closeChars.contains(c))
return false;
}
if (!currentToken.isEmpty() && currentToken.length() > 1) {
tokens.append(currentToken);
}
return tokens;
return true;
}
int LLMSuggestion::calculateReplaceLength(const QString &suggestion,
const QString &rightText,
const QString &entireLine)
int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText)
{
if (rightText.isEmpty()) {
if (rightText.isEmpty())
return 0;
const int maxN = qMin(suggestion.size(), rightText.size());
int lcp = 0;
while (lcp < maxN && suggestion.at(lcp) == rightText.at(lcp))
++lcp;
if (lcp > 0) {
if (isClosingTail(rightText, lcp))
return rightText.size();
return lcp;
}
QString structuralChars = "{}[]()<>;,";
bool hasStructuralOverlap = false;
for (const QChar &ch : structuralChars) {
if (suggestion.contains(ch) && rightText.contains(ch)) {
hasStructuralOverlap = true;
break;
}
}
if (hasStructuralOverlap) {
return rightText.length();
}
if (!isClosingTail(rightText, 0))
return 0;
static const QString closeChars = QStringLiteral("(){}[];,");
int i = suggestion.size() - 1;
while (i >= 0 && suggestion.at(i).isSpace())
--i;
if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i)))
return rightText.size();
const QStringList suggestionTokens = extractTokens(suggestion);
const QStringList lineTokens = extractTokens(entireLine);
for (const auto &token : suggestionTokens) {
if (lineTokens.contains(token)) {
return rightText.length();
}
}
return 0;
}
@@ -102,22 +69,21 @@ LLMSuggestion::LLMSuggestion(
QString rightText = blockText.mid(cursorPositionInBlock);
QString suggestionText = data.text;
QString entireLine = blockText;
if (!suggestionText.contains('\n')) {
int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine);
int replaceLength = calculateReplaceLength(suggestionText, rightText);
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
QString displayText = leftText + suggestionText + remainingRightText;
replacementDocument()->setPlainText(displayText);
} else {
int firstLineEnd = suggestionText.indexOf('\n');
QString firstLine = suggestionText.left(firstLineEnd);
QString restOfCompletion = suggestionText.mid(firstLineEnd);
int replaceLength = calculateReplaceLength(firstLine, rightText, entireLine);
int replaceLength = calculateReplaceLength(firstLine, rightText);
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
replacementDocument()->setPlainText(displayText);
}
@@ -167,10 +133,9 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
if (startPos == 0) {
QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QString entireLine = currentBlock.text();
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
int replaceLength = calculateReplaceLength(text, textAfterCursor);
if (replaceLength > 0) {
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
currentCursor.removeSelectedText();
@@ -220,9 +185,7 @@ bool LLMSuggestion::apply()
QString text = currentData.text;
QTextBlock currentBlock = cursor.block();
QString textBeforeCursor = currentBlock.text().left(cursor.positionInBlock());
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QString entireLine = currentBlock.text();
QTextCursor editCursor = cursor;
editCursor.beginEditBlock();
@@ -232,22 +195,22 @@ bool LLMSuggestion::apply()
QString firstLine = text.left(firstLineEnd);
QString restOfText = text.mid(firstLineEnd);
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine);
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor);
if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText();
}
editCursor.insertText(firstLine + restOfText);
} else {
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
int replaceLength = calculateReplaceLength(text, textAfterCursor);
if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText();
}
editCursor.insertText(text);
}

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -42,8 +42,6 @@ public:
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
bool apply() override;
static int calculateReplaceLength(const QString &suggestion,
const QString &rightText,
const QString &entireLine);
static int calculateReplaceLength(const QString &suggestion, const QString &rightText);
};
} // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@@ -1,7 +1,7 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.9.11",
"Version" : "0.9.17",
"CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -54,6 +54,90 @@ using namespace Core;
namespace QodeAssist {
namespace {
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
{
return Utils::Text::Position{pos.line, pos.column};
}
bool isIdentifierChar(QChar c)
{
return c.isLetterOrNumber() || c == QLatin1Char('_');
}
bool isInsideIdentifier(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
const QString text = block.text();
if (col <= 0 || col > text.size())
return false;
if (!isIdentifierChar(text.at(col - 1)))
return false;
return col < text.size() && isIdentifierChar(text.at(col));
}
bool isAfterMemberAccess(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
const QString text = block.text();
if (col <= 0)
return false;
int i = col - 1;
while (i >= 0 && isIdentifierChar(text.at(i)))
--i;
if (i < 0)
return false;
const QChar c = text.at(i);
if (c == QLatin1Char('.'))
return true;
if (c == QLatin1Char('>') && i >= 1 && text.at(i - 1) == QLatin1Char('-'))
return true;
if (c == QLatin1Char(':') && i >= 1 && text.at(i - 1) == QLatin1Char(':'))
return true;
return false;
}
bool isFreshIndentedLine(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
if (col == 0)
return false;
const QString leftText = block.text().left(col);
for (const QChar &ch : leftText) {
if (!ch.isSpace())
return false;
}
return true;
}
bool isAfterEagerTrigger(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
const QString text = block.text();
int i = col - 1;
while (i >= 0 && text.at(i).isSpace())
--i;
if (i < 0)
return false;
const QChar c = text.at(i);
return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(',')
|| c == QLatin1Char('=') || c == QLatin1Char('[') || c == QLatin1Char(';')
|| c == QLatin1Char(':') || c == QLatin1Char('>');
}
bool isManualMode()
{
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
}
} // anonymous namespace
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
@@ -69,10 +153,6 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_typingTimer.start();
m_hintHideTimer.setSingleShot(true);
m_hintHideTimer.setInterval(Settings::codeCompletionSettings().hintHideTimeout());
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}
@@ -108,6 +188,9 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (!Settings::codeCompletionSettings().autoCompletion())
return;
if (isManualMode())
return;
auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project))
return;
@@ -131,38 +214,29 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (charsRemoved > 0 || charsAdded <= 0) {
m_recentCharCount = 0;
m_typingTimer.restart();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
return;
}
QTextCursor cursor = widget->textCursor();
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
QString lastChar = cursor.selectedText();
const QString lastChar = cursor.selectedText();
if (lastChar.isEmpty())
return;
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
const QChar lastCh = lastChar[0];
if (lastCh == QLatin1Char('\n') || lastCh == QChar::ParagraphSeparator
|| lastCh == QChar::LineSeparator) {
m_recentCharCount = 0;
m_typingTimer.restart();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
return;
}
bool isSpaceOrTab = lastChar[0].isSpace();
bool ignoreWhitespace
const bool isSpaceOrTab = lastCh.isSpace();
const bool ignoreWhitespace
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
if (!ignoreWhitespace || !isSpaceOrTab) {
if (!ignoreWhitespace || !isSpaceOrTab)
m_recentCharCount += charsAdded;
}
if (m_typingTimer.elapsed()
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
@@ -170,13 +244,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
m_typingTimer.restart();
}
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode == 1) {
handleAutoRequestTrigger(widget, charsAdded, isSpaceOrTab);
} else {
handleHintBasedTrigger(widget, charsAdded, isSpaceOrTab, cursor);
}
handleAutoRequestTrigger(widget);
});
}
@@ -205,11 +273,9 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return;
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 0) {
const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
editor->abortAssist();
}
const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{
@@ -270,33 +336,29 @@ void QodeAssistClient::requestQuickRefactor(
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
{
cancelRunningRequest(editor);
if (m_runningRequests.contains(editor)) {
if (Settings::codeCompletionSettings().cancelOnInput())
cancelRunningRequest(editor);
else
return;
}
auto it = m_scheduledRequests.find(editor);
if (it == m_scheduledRequests.end()) {
auto timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this, editor]() {
if (editor
&& editor->textCursor().position()
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
&& m_recentCharCount
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
requestCompletions(editor);
if (!editor || m_runningRequests.contains(editor))
return;
if (editor->textCursor().position()
!= m_scheduledRequests[editor]->property("cursorPosition").toInt())
return;
requestCompletions(editor);
});
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
delete m_scheduledRequests.take(editor);
cancelRunningRequest(editor);
});
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
cancelRunningRequest(editor);
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
});
it = m_scheduledRequests.insert(editor, timer);
}
@@ -307,11 +369,9 @@ void QodeAssistClient::handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
{
m_progressHandler.hideProgress();
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 1) {
const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
editor->abortAssist();
}
if (response.error()) {
log(*response.error());
@@ -325,12 +385,25 @@ void QodeAssistClient::handleCompletions(
requestPosition = requestParams->position().toPositionInDocument(editor->document());
const MultiTextCursor cursors = editor->multiTextCursor();
if (cursors.hasMultipleCursors())
if (cursors.hasMultipleCursors() || cursors.hasSelection())
return;
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition)
const int currentPosition = cursors.mainCursor().position();
if (requestPosition < 0 || currentPosition < requestPosition)
return;
QString typedSinceRequest;
if (currentPosition > requestPosition) {
QTextCursor diffCursor(editor->document());
diffCursor.setPosition(requestPosition);
diffCursor.setPosition(currentPosition, QTextCursor::KeepAnchor);
typedSinceRequest = diffCursor.selectedText();
if (typedSinceRequest.contains(QChar::ParagraphSeparator)
|| typedSinceRequest.contains(QLatin1Char('\n'))) {
return;
}
}
if (const std::optional<GetCompletionResponse> result = response.result()) {
auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty();
@@ -338,34 +411,58 @@ void QodeAssistClient::handleCompletions(
QList<Completion> completions
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
QList<Completion> matchedCompletions;
matchedCompletions.reserve(completions.size());
for (Completion &completion : completions) {
const LanguageServerProtocol::Range range = completion.range();
if (range.start().line() != range.end().line())
continue;
const QString completionText = completion.text();
QString completionText = completion.text();
const int end = int(completionText.size()) - 1;
int delta = 0;
while (delta <= end && completionText[end - delta].isSpace())
++delta;
if (delta > 0)
completion.setText(completionText.chopped(delta));
completionText.chop(delta);
if (!typedSinceRequest.isEmpty()) {
if (!completionText.startsWith(typedSinceRequest))
continue;
completionText = completionText.mid(typedSinceRequest.size());
if (completionText.isEmpty())
continue;
}
completion.setText(completionText);
matchedCompletions.append(completion);
}
auto suggestions = Utils::transform(completions, [](const Completion &c) {
if (matchedCompletions.isEmpty()) {
LOG_MESSAGE("No valid completions received");
return;
}
const Text::Position anchor = typedSinceRequest.isEmpty()
? Text::Position{}
: Text::Position::fromPositionInDocument(editor->document(), currentPosition);
const bool useAnchor = !typedSinceRequest.isEmpty();
auto suggestions = Utils::transform(matchedCompletions,
[useAnchor, &anchor](const Completion &c) {
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()};
};
if (useAnchor) {
return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
}
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
if (completions.isEmpty()) {
LOG_MESSAGE("No valid completions received");
return;
}
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
}
}
@@ -376,12 +473,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
if (it == m_runningRequests.constEnd())
return;
m_progressHandler.hideProgress();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
cancelRequest(it->id());
m_runningRequests.erase(it);
}
@@ -423,17 +514,6 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear();
}
bool QodeAssistClient::isHintVisible() const
{
return m_hintHandler.isHintVisible();
}
void QodeAssistClient::hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor)
{
m_hintHandler.hideHint();
requestCompletions(editor);
}
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
m_progressHandler.hideProgress();
@@ -465,13 +545,6 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
}
}
namespace {
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
{
return Utils::Text::Position{pos.line, pos.column};
}
} // anonymous namespace
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
@@ -604,58 +677,20 @@ void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor
editCursor.endEditBlock();
}
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
int charsAdded,
bool isSpaceOrTab)
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
{
Q_UNUSED(isSpaceOrTab);
const QTextCursor cursor = widget->textCursor();
const auto &settings = Settings::codeCompletionSettings();
const bool smart = settings.smartContextTrigger();
if (m_recentCharCount
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
return;
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
const int charThreshold = settings.autoCompletionCharThreshold();
if (eager || m_recentCharCount > charThreshold)
scheduleRequest(widget);
}
}
void QodeAssistClient::handleHintBasedTrigger(TextEditor::TextEditorWidget *widget,
int charsAdded,
bool isSpaceOrTab,
QTextCursor &cursor)
{
Q_UNUSED(charsAdded);
const int hintThreshold = Settings::codeCompletionSettings().hintCharThreshold();
if (m_recentCharCount >= hintThreshold && !isSpaceOrTab) {
const QRect cursorRect = widget->cursorRect(cursor);
QPoint globalPos = widget->viewport()->mapToGlobal(cursorRect.topLeft());
QPoint localPos = widget->mapFromGlobal(globalPos);
int fontSize = widget->font().pixelSize();
if (fontSize <= 0) {
fontSize = widget->fontMetrics().height();
}
QTextCursor textCursor = widget->textCursor();
if (m_recentCharCount <= hintThreshold) {
textCursor
.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, m_recentCharCount);
} else {
textCursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, hintThreshold);
}
int x = localPos.x() + cursorRect.height();
int y = localPos.y() + cursorRect.height() / 4;
QPoint hintPos(x, y);
if (!m_hintHandler.isHintVisible()) {
m_hintHandler.showHint(widget, hintPos, fontSize);
} else {
m_hintHandler.updateHintPosition(widget, hintPos);
}
m_hintHideTimer.start();
}
}
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
@@ -667,46 +702,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
// Check hint trigger key (0=Space, 1=Ctrl+Space, 2=Alt+Space, 3=Ctrl+Enter, 4=Tab, 5=Enter)
if (m_hintHandler.isHintVisible()) {
const int triggerKeyIndex = Settings::codeCompletionSettings().hintTriggerKey();
bool isMatchingKey = false;
const Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
switch (triggerKeyIndex) {
case 0: // Space
isMatchingKey = (keyEvent->key() == Qt::Key_Space
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
break;
case 1: // Ctrl+Space
isMatchingKey = (keyEvent->key() == Qt::Key_Space
&& (modifiers & Qt::ControlModifier));
break;
case 2: // Alt+Space
isMatchingKey = (keyEvent->key() == Qt::Key_Space
&& (modifiers & Qt::AltModifier));
break;
case 3: // Ctrl+Enter
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
&& (modifiers & Qt::ControlModifier));
break;
case 4: // Tab
isMatchingKey = (keyEvent->key() == Qt::Key_Tab);
break;
case 5: // Enter
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
break;
}
if (isMatchingKey) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
requestCompletions(editor);
return true;
}
}
if (keyEvent->key() == Qt::Key_Escape) {
if (m_runningRequests.contains(editor)) {
cancelRunningRequest(editor);
@@ -724,8 +719,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
}
m_progressHandler.hideProgress();
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
}

View File

@@ -1,26 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -32,12 +12,11 @@
#include "RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp"
#include "widgets/CompletionHintHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
namespace QodeAssist {
@@ -54,9 +33,6 @@ public:
void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
bool isHintVisible() const;
void hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
@@ -75,8 +51,7 @@ private:
void displayRefactoringWidget(const RefactorResult &result);
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@@ -85,10 +60,8 @@ private:
QElapsedTimer m_typingTimer;
int m_recentCharCount;
QTimer m_hintHideTimer;
CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler;
CompletionHintHandler m_hintHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -26,4 +10,16 @@ const char MENU_ID[] = "QodeAssist.Menu";
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

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,40 +1,25 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "QuickRefactorHandler.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <context/DocumentContextReader.hpp>
#include <llmcore/ResponseCleaner.hpp>
#include <pluginllmcore/ResponseCleaner.hpp>
#include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp>
#include <llmcore/PromptTemplateManager.hpp>
#include <llmcore/ProvidersManager.hpp>
#include <llmcore/RequestConfig.hpp>
#include <llmcore/RulesLoader.hpp>
#include <pluginllmcore/PromptTemplateManager.hpp>
#include <pluginllmcore/ProvidersManager.hpp>
#include <pluginllmcore/RulesLoader.hpp>
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp>
#include <settings/ToolsSettings.hpp>
namespace QodeAssist {
@@ -109,8 +94,8 @@ void QuickRefactorHandler::prepareAndSendRequest(
{
auto &settings = Settings::generalSettings();
auto &providerRegistry = LLMCore::ProvidersManager::instance();
auto &promptManager = LLMCore::PromptTemplateManager::instance();
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
const auto providerName = settings.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
@@ -140,70 +125,64 @@ void QuickRefactorHandler::prepareAndSendRequest(
return;
}
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::QuickRefactoring;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint());
config.apiKey = provider->apiKey();
QJsonObject payload{
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3")
.arg(
Settings::generalSettings().qrUrl(),
Settings::generalSettings().qrModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().qrUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
}
LLMCore::ContextData context = prepareContext(editor, range, instructions);
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
bool enableTools = Settings::quickRefactorSettings().useTools();
bool enableThinking = Settings::quickRefactorSettings().useThinking();
provider->prepareRequest(
config.providerRequest,
payload,
promptTemplate,
context,
LLMCore::RequestType::QuickRefactoring,
PluginLLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
QString requestId = QUuid::createUuid().toString();
m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}};
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
m_isRefactoringInProgress = true;
m_activeRequests[requestId] = {request, provider};
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
provider->client(),
&::LLMQore::BaseClient::requestFinalized,
this,
&QuickRefactorHandler::handleRequestFinalized,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&QuickRefactorHandler::handleRequestFailed,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider};
}
LLMCore::ContextData QuickRefactorHandler::prepareContext(
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
{
LLMCore::ContextData context;
PluginLLMCore::ContextData context;
auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader;
@@ -287,10 +266,10 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
auto project = LLMCore::RulesLoader::getActiveProject();
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
project, LLMCore::RulesContext::QuickRefactor);
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
project, PluginLLMCore::RulesContext::QuickRefactor);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
@@ -368,7 +347,7 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
context.systemPrompt = systemPrompt;
QVector<LLMCore::Message> messages;
QVector<PluginLLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
@@ -387,7 +366,7 @@ void QuickRefactorHandler::handleLLMResponse(
if (isComplete) {
m_isRefactoringInProgress = false;
QString cleanedResponse = LLMCore::ResponseCleaner::clean(response);
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
RefactorResult result;
result.newText = cleanedResponse;
@@ -436,6 +415,22 @@ 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)
{
if (requestId == m_lastRequestId) {

View File

@@ -1,34 +1,19 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonObject>
#include <QObject>
#include <LLMQore/BaseClient.hpp>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <llmcore/ContextData.hpp>
#include <llmcore/Provider.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/Provider.hpp>
namespace QodeAssist {
@@ -59,6 +44,8 @@ signals:
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFinalized(
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
void handleRequestFailed(const QString &requestId, const QString &error);
private:
@@ -68,7 +55,7 @@ private:
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
LLMCore::ContextData prepareContext(
PluginLLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
@@ -76,7 +63,7 @@ private:
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
PluginLLMCore::Provider *provider;
};
QHash<QString, RequestContext> m_activeRequests;

146
README.md
View File

@@ -1,10 +1,12 @@
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
# QodeAssist AI coding assistant for Qt Creator
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is a comprehensive AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion, interactive chat with multiple interface options, inline quick refactoring, and AI function calling capabilities for C++ and QML development. Supporting both local providers (Ollama, llama.cpp, LM Studio) and cloud services (Claude, OpenAI, Google AI, Mistral AI), QodeAssist enhances your productivity with context-aware AI assistance, project-specific rules, and extensive customization options directly in your Qt development environment.
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
[![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist?label=release)](https://github.com/Palm1r/QodeAssist/releases)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71&label=downloads)
[![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)
![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**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
@@ -29,13 +31,16 @@
QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel)
- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library
- **File Context**: Attach or link files for better AI understanding
- **Tool Calling**: AI can read project files, search code, and access diagnostics
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more
- **Customizable**: Project-specific rules, custom instructions, and extensive model templates
- **Code Completion** — intelligent, context-aware suggestions (FIM and chat models) for C++ and QML, with multiline support
- **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
- **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 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
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, any OpenAI-compatible endpoint
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
@@ -49,6 +54,11 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</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>
<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">
@@ -147,14 +157,19 @@ All settings (provider, model, template, URL) are configured automatically. Just
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
- **[Anthropic Claude](docs/claude-configuration.md)** - Cloud provider (manual setup)
- **[OpenAI](docs/openai-configuration.md)** - Cloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Cloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Cloud provider
- **LM Studio** - Local LLM provider
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
**Local providers:**
- **[Ollama](docs/ollama-configuration.md)** — native Ollama API
- **Ollama (OpenAI-compatible)** — Ollama's `/v1` endpoint for tool-calling models
- **[llama.cpp](docs/llamacpp-configuration.md)** — local `llama-server`
- **LM Studio** — OpenAI-compatible Chat API
- **LM Studio (Responses API)** — newer models that require the Responses endpoint
**Cloud providers:**
- **[Anthropic Claude](docs/claude-configuration.md)** — manual setup guide
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
- **[Google AI](docs/google-ai-configuration.md)** — Gemini
- **OpenAI-compatible** — OpenRouter and any custom endpoint
### Recommended Models for Best Experience
@@ -228,10 +243,72 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- **[Learn more](docs/quick-refactoring.md)**
### Tools & Function Calling
- Read project files
- List and search in project
- Access linter/compiler issues
- Enabled by default (can be disabled)
Chat and Quick Refactor can call tools to inspect and modify your project. Each tool can be individually enabled/disabled in settings.
| Tool | What it does |
|------|--------------|
| `list_project_files` | List files in the active project(s) |
| `find_file` | Find a file by name or partial path |
| `read_file` | Read file contents (project or absolute path) |
| `search_project` | Grep / symbol search across project sources |
| `create_new_file` | Create a new empty file on disk |
| `edit_file` | Replace content in a file (old → new) |
| `build_project` | Build the active project and return compiler output |
| `get_issues_list` | Read current linter / compiler diagnostics |
| `execute_terminal_command` | Run a shell command (with confirmation) |
| `todo_tool` | Track multi-step task progress during a conversation |
### 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
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
- **Enable** in `Tools → Options → QodeAssist → MCP Server`
- **Transport**: HTTP + SSE by default; a stdio bridge is provided for clients that only speak stdio (e.g. Claude Desktop)
- **Ready-to-copy snippets** for Claude Code, VS Code, and the bridge are available via the "Show connection instructions" button in settings
### MCP Client Hub
QodeAssist can also act as an **MCP client**, connecting to external MCP servers and making their tools available to Chat and Quick Refactor alongside the built-in ones.
- **Configure** servers in `Tools → Options → QodeAssist → MCP Client`
- **Transports**: stdio and HTTP/SSE
- **Limitation**: authenticated MCP servers (OAuth / token-protected) are **not supported yet** — only servers that accept unauthenticated local connections work for now
## Context Layers
@@ -413,14 +490,17 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
## Development Progress
- [x] Code completion functionality
- [x] Chat assistant with multiple panels
- [x] Code completion (FIM and chat models)
- [x] Chat assistant (side / bottom / detached panels)
- [x] Quick refactoring with custom-instructions library
- [x] Diff sharing with models
- [x] Tools/function calling support
- [x] Project-specific rules
- [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] MCP (Model Context Protocol) — QodeAssist as a server
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
- [ ] Full project source sharing
- [ ] Additional provider support
- [ ] MCP (Model Context Protocol) support
## Support the development of QodeAssist
If you find QodeAssist helpful, there are several ways you can support the project:
@@ -432,6 +512,7 @@ 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.
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`
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
@@ -439,6 +520,11 @@ If you find QodeAssist helpful, there are several ways you can support the proje
Every contribution, no matter how small, is greatly appreciated and helps keep the project alive!
## Related Projects
- **[LLMQore](https://github.com/Palm1r/llmqore)** — the standalone LLM-core library extracted from QodeAssist, reusable in other Qt/C++ projects
- **[QodeAssistUpdater](https://github.com/Palm1r/QodeAssistUpdater)** — CLI installer/updater for the plugin
## How to Build
### Prerequisites

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "RefactorSuggestion.hpp"
#include "LLMSuggestion.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "RefactorSuggestionHoverHandler.hpp"
#include "RefactorSuggestion.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https:
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "FlowEditor.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https:
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "GridBackground.hpp"
#include <QPainter>

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "BaseTask.hpp"
#include "TaskPort.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "Flow.hpp"
#include "TaskPort.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "FlowManager.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "TaskConnection.hpp"
#include "BaseTask.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "TaskPort.hpp"
#include "TaskConnection.hpp"

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@@ -1,21 +1,5 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "TaskRegistry.hpp"

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