Compare commits

..

54 Commits

Author SHA1 Message Date
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
Petr Mironychev
9a2ba08538 chore: Upgrade plugin to 0.9.11 2026-03-13 00:56:25 +01:00
Petr Mironychev
37084bec59 feat: Improve execute terminal command tool 2026-03-13 00:34:20 +01:00
Petr Mironychev
6910037e97 feat: Update models configuration 2026-03-12 23:58:06 +01:00
Petr Mironychev
a72cdd85a4 feat: Add support QtC 19
remove support QtC 17
2026-03-12 23:31:35 +01:00
lebedeviv1988
31b4e73af5 fix: Qt Creator 19 API breaking changes (#328)
* Inherits `QodeAssist::Settings::AgentRolesWidget` from `Core::IOptionsPageWidget`

* Adds `QodeAssist::Settings::showSettings` function and use it instead `Core::ICore::showOptionsDialog`

---------

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 16:00:51 +01:00
lebedeviv1988
088887c802 fix: enables the send message shortcut only for active chat (#322)
fix: Disables sending message shortcut instead of filtering in `Shortcut::activated` signal handler

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 12:01:14 +01:00
lebedeviv1988
b7a9787cc3 refactor: Refactors AgentRoleDialog's modes handling (#325)
* fix: Fixes `undefined-bool-conversion` compilation warning.

* refactor: Replaces `AgentRoleDialog::m_editMode` with `AgentRoleDialog::m_action`

---------

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 10:48:01 +01:00
Petr Mironychev
e2e13f0f38 refactor: Improve http client (#319) 2026-02-25 15:13:05 +01:00
Petr Mironychev
49ae335d7d chore: Update plugin to 0.9.10 2026-02-25 12:33:14 +01:00
Petr Mironychev
2ba58a403f refactor: UI for opening content from chat (#318)
* refactor: Changed options to opening images from chat
* refactor: Add customizable tooltip
2026-02-25 07:49:37 +01:00
Petr Mironychev
3de1619bf0 feat: Add file search to chat (#317) 2026-02-22 13:53:44 +01:00
Petr Mironychev
ec45067336 chore: Upgrade plugin to 0.9.9 version 2026-01-27 22:41:57 +01:00
Petr Mironychev
52fb65c5b1 feat: Add support QtCreator 18.0.2 2026-01-27 22:41:20 +01:00
Petr Mironychev
478f369ad2 feat: Add codestral and mistral quick setup 2026-01-27 22:41:02 +01:00
Petr Mironychev
762c965377 fix: Add preconditions for windows chat 2026-01-27 22:35:02 +01:00
Petr Mironychev
d2b93310e2 chore: Update plugin to 0.9.8 2026-01-20 20:00:49 +01:00
Petr Mironychev
f3b1e7f411 Add quick setup screenshot 2026-01-20 19:57:44 +01:00
Petr Mironychev
a55c6ccfdb feat: Add predefined templates 2026-01-20 19:54:16 +01:00
Petr Mironychev
b32433c336 refactor: Change quick refactor ui layout 2026-01-20 18:08:49 +01:00
Petr Mironychev
6f11260cd1 refactor: Change UI for fix behavior 2026-01-19 23:52:44 +01:00
Petr Mironychev
ddd6aba091 fix: Remove close chat action from editor context menu 2026-01-19 23:17:31 +01:00
Dinesh Bala
e3f464c54e fix: Create _content folder only when there is an attachment (#297) 2025-12-16 13:19:10 +01:00
Petr Mironychev
e86e58337a Update QodeAssist version range for Qt Creator 16.0.2 2025-12-15 01:00:00 +01:00
Petr Mironychev
dbd47387be chore: Update plugin to 0.9.7 2025-12-15 00:47:50 +01:00
Petr Mironychev
50e1276ab2 feat: Add support QtC 18.0.1 (#296)
* feat: Add support QtC 18.0.1
* feat: Remove support QtC 16.0.2
2025-12-14 02:53:58 +01:00
Petr Mironychev
50c948ccfe chore: Update plugin to 0.9.6 version 2025-12-08 11:10:20 +01:00
Petr Mironychev
949dad4fd2 feat: Update built in roles and docs 2025-12-08 11:09:21 +01:00
Petr Mironychev
01fd7dad6f chore: Upgrade plugin to 0.9.5 version 2025-12-08 10:25:38 +01:00
Petr Mironychev
fd408ba415 fix: Change compressing icon place 2025-12-08 10:24:27 +01:00
Petr Mironychev
14e7ea2ec3 feat: Add separator to chat top bar 2025-12-08 10:18:12 +01:00
Petr Mironychev
9f050aec67 feat: Add summarize chat (#289) 2025-12-05 11:08:23 +01:00
Petr Mironychev
9e118ddfaf fix: Add pause between call tools 2025-12-04 21:05:13 +01:00
Petr Mironychev
157498b770 revert: Remove compact mode for chat blocks 2025-12-04 20:51:14 +01:00
Petr Mironychev
5c8a8f305d Fix chat scrolling (#288)
* fix: Change chat scrolling behavior
* feat: Add compact mode for chat blocks
2025-12-04 20:00:53 +01:00
Petr Mironychev
fc33bb60d0 feat: Add agent roles (#287)
* feat: Add agent roles
* doc: Add agent roles to docs
2025-12-04 19:41:30 +01:00
Petr Mironychev
498eb4d932 feat: Add todo tool (#286) 2025-12-03 21:28:59 +01:00
Petr Mironychev
fb941cea99 doc: Update docs for using OpenAI GPT-5.1 and codex 2025-12-01 16:08:21 +01:00
364 changed files with 10226 additions and 13677 deletions

View File

@@ -46,20 +46,18 @@ jobs:
} }
qt_config: qt_config:
- { - {
qt_version: "6.8.3", qt_version: "6.10.1",
qt_creator_version: "16.0.2" qt_creator_version: "18.0.2"
} }
- { - {
qt_version: "6.9.2", qt_version: "6.10.2",
qt_creator_version: "17.0.2" qt_creator_version: "19.0.0"
}
- {
qt_version: "6.10.0",
qt_creator_version: "18.0.0"
} }
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
with:
submodules: recursive
- name: Checkout submodules - name: Checkout submodules
id: git id: git

2
.gitignore vendored
View File

@@ -78,3 +78,5 @@ CMakeLists.txt.user*
/.cursor /.cursor
/.vscode /.vscode
.qtc_clangd/compile_commands.json .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

@@ -34,7 +34,8 @@ add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH} -DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
) )
add_subdirectory(llmcore) add_subdirectory(sources/external/llmqore)
add_subdirectory(pluginllmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(UIControls) add_subdirectory(UIControls)
@@ -61,6 +62,8 @@ add_qtc_plugin(QodeAssist
QtCreator::ExtensionSystem QtCreator::ExtensionSystem
QtCreator::Utils QtCreator::Utils
QtCreator::CPlusPlus QtCreator::CPlusPlus
LLMQore
PluginLLMCore
QodeAssistChatViewplugin QodeAssistChatViewplugin
SOURCES SOURCES
.github/workflows/build_cmake.yml .github/workflows/build_cmake.yml
@@ -93,10 +96,12 @@ add_qtc_plugin(QodeAssist
templates/OpenAIResponses.hpp templates/OpenAIResponses.hpp
providers/Providers.hpp providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
@@ -112,7 +117,6 @@ add_qtc_plugin(QodeAssist
providers/OpenAIResponses/ItemTypesReference.hpp providers/OpenAIResponses/ItemTypesReference.hpp
providers/OpenAIResponsesRequestBuilder.hpp providers/OpenAIResponsesRequestBuilder.hpp
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
providers/OpenAIResponsesMessage.hpp providers/OpenAIResponsesMessage.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
@@ -141,23 +145,22 @@ add_qtc_plugin(QodeAssist
widgets/DiffStatistics.hpp widgets/DiffStatistics.hpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp tools/ToolsRegistration.hpp tools/ToolsRegistration.cpp
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
tools/ToolHandler.hpp tools/ToolHandler.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
tools/ToolsManager.hpp tools/ToolsManager.cpp
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
tools/EditFileTool.hpp tools/EditFileTool.cpp tools/EditFileTool.hpp tools/EditFileTool.cpp
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.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/FileSearchUtils.hpp tools/FileSearchUtils.cpp
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp tools/TodoTool.hpp tools/TodoTool.cpp
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp mcp/McpServerManager.hpp mcp/McpServerManager.cpp
providers/OllamaMessage.hpp providers/OllamaMessage.cpp mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
providers/GoogleMessage.hpp providers/GoogleMessage.cpp mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
) )
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)

View File

@@ -20,8 +20,9 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/AttachedFilesPlace.qml qml/controls/AttachedFilesPlace.qml
qml/controls/BottomBar.qml qml/controls/BottomBar.qml
qml/controls/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml qml/controls/FileEditsActionBar.qml
qml/controls/RulesViewer.qml qml/controls/ContextViewer.qml
qml/controls/Toast.qml qml/controls/Toast.qml
qml/controls/TopBar.qml qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml qml/controls/SplitDropZone.qml
@@ -43,6 +44,7 @@ qt_add_qml_module(QodeAssistChatView
icons/chat-icon.svg icons/chat-icon.svg
icons/chat-pause-icon.svg icons/chat-pause-icon.svg
icons/rules-icon.svg icons/rules-icon.svg
icons/context-icon.svg
icons/open-in-editor.svg icons/open-in-editor.svg
icons/apply-changes-button.svg icons/apply-changes-button.svg
icons/undo-changes-button.svg icons/undo-changes-button.svg
@@ -52,6 +54,7 @@ qt_add_qml_module(QodeAssistChatView
icons/tools-icon-on.svg icons/tools-icon-on.svg
icons/tools-icon-off.svg icons/tools-icon-off.svg
icons/settings-icon.svg icons/settings-icon.svg
icons/compress-icon.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
@@ -65,6 +68,8 @@ qt_add_qml_module(QodeAssistChatView
ChatData.hpp ChatData.hpp
FileItem.hpp FileItem.cpp FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp
FileMentionItem.hpp FileMentionItem.cpp
) )
target_link_libraries(QodeAssistChatView target_link_libraries(QodeAssistChatView
@@ -75,11 +80,12 @@ target_link_libraries(QodeAssistChatView
Qt::Network Qt::Network
QtCreator::Core QtCreator::Core
QtCreator::Utils QtCreator::Utils
LLMCore PluginLLMCore
QodeAssistSettings QodeAssistSettings
Context Context
QodeAssistUIControlsplugin QodeAssistUIControlsplugin
QodeAssistLogger QodeAssistLogger
LLMQore
) )
target_include_directories(QodeAssistChatView target_include_directories(QodeAssistChatView

290
ChatView/ChatCompressor.cpp Normal file
View File

@@ -0,0 +1,290 @@
// 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"
#include "ProvidersManager.hpp"
#include "logger/Logger.hpp"
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
namespace QodeAssist::Chat {
ChatCompressor::ChatCompressor(QObject *parent)
: QObject(parent)
{}
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
{
if (m_isCompressing) {
emit compressionFailed(tr("Compression already in progress"));
return;
}
if (chatFilePath.isEmpty()) {
emit compressionFailed(tr("No chat file to compress"));
return;
}
if (!chatModel || chatModel->rowCount() == 0) {
emit compressionFailed(tr("Chat is empty, nothing to compress"));
return;
}
auto providerName = Settings::generalSettings().caProvider();
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
return;
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) {
emit compressionFailed(tr("No template available"));
return;
}
m_isCompressing = true;
m_chatModel = chatModel;
m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear();
emit compressionStarted();
connectProviderSignals();
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));
}
bool ChatCompressor::isCompressing() const
{
return m_isCompressing;
}
void ChatCompressor::cancelCompression()
{
if (!m_isCompressing)
return;
LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState();
emit compressionFailed(tr("Compression cancelled"));
}
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
{
if (!m_isCompressing || requestId != m_currentRequestId)
return;
m_accumulatedSummary += partialText;
}
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
{
Q_UNUSED(fullText)
if (!m_isCompressing || requestId != m_currentRequestId)
return;
LOG_MESSAGE(
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
QString compressedPath = createCompressedChatPath(m_originalChatPath);
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
handleCompressionError(tr("Failed to save compressed chat"));
return;
}
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
emit compressionCompleted(compressedPath);
}
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
{
if (!m_isCompressing || requestId != m_currentRequestId)
return;
LOG_MESSAGE(QString("Compression request failed: %1").arg(error));
handleCompressionError(tr("Compression failed: %1").arg(error));
}
void ChatCompressor::handleCompressionError(const QString &error)
{
cleanupState();
emit compressionFailed(error);
}
QString ChatCompressor::createCompressedChatPath(const QString &originalPath) const
{
QFileInfo fileInfo(originalPath);
QString hash = QString::number(QDateTime::currentMSecsSinceEpoch() % 100000, 16);
return QString("%1/%2_%3.%4")
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
}
QString ChatCompressor::buildCompressionPrompt() const
{
return QStringLiteral(
"Please create a comprehensive summary of our entire conversation above. "
"The summary should:\n"
"1. Preserve all important context, decisions, and key information\n"
"2. Maintain technical details, code snippets, file references, and specific examples\n"
"3. Keep the chronological flow of the discussion\n"
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
"5. Be written in clear, structured format\n"
"6. Use markdown formatting for better readability\n\n"
"Create the summary now:");
}
void ChatCompressor::buildRequestPayload(
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
{
PluginLLMCore::ContextData context;
context.systemPrompt = QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of discussion.");
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool
|| msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
PluginLLMCore::Message apiMessage;
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
apiMessage.content = msg.content;
messages.append(apiMessage);
}
PluginLLMCore::Message compressionRequest;
compressionRequest.role = "user";
compressionRequest.content = buildCompressionPrompt();
messages.append(compressionRequest);
context.history = messages;
m_provider->prepareRequest(
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
}
bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary)
{
QFile sourceFile(sourcePath);
if (!sourceFile.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
return false;
}
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
sourceFile.close();
if (doc.isNull() || !doc.isObject()) {
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
.arg(sourcePath, parseError.errorString()));
return false;
}
QJsonObject root = doc.object();
QJsonObject summaryMessage;
summaryMessage["role"] = "assistant";
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
summaryMessage["isRedacted"] = false;
summaryMessage["attachments"] = QJsonArray();
summaryMessage["images"] = QJsonArray();
root["messages"] = QJsonArray{summaryMessage};
if (QFile::exists(destPath))
QFile::remove(destPath);
QFile destFile(destPath);
if (!destFile.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to create compressed chat file: %1").arg(destPath));
return false;
}
destFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
return true;
}
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));
}
void ChatCompressor::disconnectAllSignals()
{
for (const auto &connection : std::as_const(m_connections))
disconnect(connection);
m_connections.clear();
}
void ChatCompressor::cleanupState()
{
disconnectAllSignals();
m_isCompressing = false;
m_currentRequestId.clear();
m_originalChatPath.clear();
m_accumulatedSummary.clear();
m_chatModel = nullptr;
m_provider = nullptr;
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,63 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonObject>
#include <QList>
#include <QObject>
#include <QString>
namespace QodeAssist::PluginLLMCore {
class Provider;
class PromptTemplate;
} // namespace QodeAssist::PluginLLMCore
namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject
{
Q_OBJECT
public:
explicit ChatCompressor(QObject *parent = nullptr);
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
bool isCompressing() const;
void cancelCompression();
signals:
void compressionStarted();
void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error);
private slots:
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
void onFullResponseReceived(const QString &requestId, const QString &fullText);
void onRequestFailed(const QString &requestId, const QString &error);
private:
QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const;
bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState();
void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false;
QString m_currentRequestId;
QString m_originalChatPath;
QString m_accumulatedSummary;
PluginLLMCore::Provider *m_provider = nullptr;
ChatModel *m_chatModel = nullptr;
QList<QMetaObject::Connection> m_connections;
};
} // namespace QodeAssist::Chat

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include <utils/aspects.h> #include <utils/aspects.h>
@@ -117,8 +101,10 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
QString contentFolder = QDir(dirPath).filePath(baseName + "_content"); QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
QString fullPath = QDir(contentFolder).filePath(image.storedPath); QString fullPath = QDir(contentFolder).filePath(image.storedPath);
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString(); imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
imageMap["filePath"] = fullPath;
} else { } else {
imageMap["imageUrl"] = QString(); imageMap["imageUrl"] = QString();
imageMap["filePath"] = QString();
} }
imagesList.append(imageMap); imagesList.append(imageMap);

View File

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

View File

@@ -1,29 +1,16 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ChatRootView.hpp" #include "ChatRootView.hpp"
#include <QClipboard> #include <QClipboard>
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QTextStream>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
@@ -34,7 +21,9 @@
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include "AgentRole.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatCompressor.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
@@ -46,17 +35,18 @@
#include "context/ChangesManager.h" #include "context/ChangesManager.h"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
#include "llmcore/RulesLoader.hpp" #include "pluginllmcore/RulesLoader.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(LLMCore::PromptTemplateManager::instance()) , m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this)) , m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_fileManager(new ChatFileManager(this)) , m_fileManager(new ChatFileManager(this))
, m_isRequestInProgress(false) , m_isRequestInProgress(false)
, m_chatCompressor(new ChatCompressor(this))
{ {
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles(); m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect( connect(
@@ -117,6 +107,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
this, this,
&ChatRootView::updateInputTokensCount); &ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::baseSystemPromptChanged);
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@@ -209,6 +204,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
updateInputTokensCount(); updateInputTokensCount();
refreshRules(); refreshRules();
loadAvailableConfigurations(); loadAvailableConfigurations();
loadAvailableAgentRoles();
connect( connect(
ProjectExplorer::ProjectManager::instance(), ProjectExplorer::ProjectManager::instance(),
@@ -216,6 +212,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::refreshRules); &ChatRootView::refreshRules);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded,
this,
&ChatRootView::openFilesChanged);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectRemoved,
this,
&ChatRootView::openFilesChanged);
connect( connect(
&Settings::chatAssistantSettings().enableChatTools, &Settings::chatAssistantSettings().enableChatTools,
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
@@ -238,6 +246,27 @@ ChatRootView::ChatRootView(QQuickItem *parent)
m_lastErrorMessage = error; m_lastErrorMessage = error;
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
}); });
// ChatCompressor signals
connect(m_chatCompressor, &ChatCompressor::compressionStarted, this, [this]() {
emit isCompressingChanged();
});
connect(m_chatCompressor, &ChatCompressor::compressionCompleted, this, [this](const QString &compressedChatPath) {
emit isCompressingChanged();
m_lastInfoMessage = tr("Chat compressed successfully!");
emit lastInfoMessageChanged();
emit compressionCompleted(compressedChatPath);
loadHistory(compressedChatPath);
});
connect(m_chatCompressor, &ChatCompressor::compressionFailed, this, [this](const QString &error) {
emit isCompressingChanged();
m_lastErrorMessage = error;
emit lastErrorMessageChanged();
emit compressionFailed(error);
});
} }
ChatModel *ChatRootView::chatModel() const ChatModel *ChatRootView::chatModel() const
@@ -311,6 +340,12 @@ void ChatRootView::clearLinkedFiles()
emit linkedFilesChanged(); emit linkedFilesChanged();
} }
void ChatRootView::clearMessages()
{
m_clientInterface->clearMessages();
clearLinkedFiles();
}
QString ChatRootView::getChatsHistoryDir() const QString ChatRootView::getChatsHistoryDir() const
{ {
QString path; QString path;
@@ -699,7 +734,17 @@ void ChatRootView::openRulesFolder()
void ChatRootView::openSettings() void ChatRootView::openSettings()
{ {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID); QMetaObject::invokeMethod(
this,
[]() { Settings::showSettings(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID); },
Qt::QueuedConnection);
}
void ChatRootView::openFileInEditor(const QString &filePath)
{
if (filePath.isEmpty())
return;
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
} }
void ChatRootView::updateInputTokensCount() void ChatRootView::updateInputTokensCount()
@@ -752,6 +797,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
if (editor) { if (editor) {
m_currentEditors.removeOne(editor); m_currentEditors.removeOne(editor);
} }
emit openFilesChanged();
} }
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor) void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
@@ -769,6 +816,7 @@ void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath
{ {
if (editor && editor->document()) { if (editor && editor->document()) {
m_currentEditors.append(editor); m_currentEditors.append(editor);
emit openFilesChanged();
} }
} }
@@ -865,7 +913,7 @@ QString ChatRootView::getRuleContent(int index)
if (index < 0 || index >= m_activeRules.size()) if (index < 0 || index >= m_activeRules.size())
return QString(); return QString();
return LLMCore::RulesLoader::loadRuleFileContent( return PluginLLMCore::RulesLoader::loadRuleFileContent(
m_activeRules[index].toMap()["filePath"].toString()); m_activeRules[index].toMap()["filePath"].toString());
} }
@@ -873,7 +921,7 @@ void ChatRootView::refreshRules()
{ {
m_activeRules.clear(); m_activeRules.clear();
auto project = LLMCore::RulesLoader::getActiveProject(); auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (!project) { if (!project) {
emit activeRulesChanged(); emit activeRulesChanged();
emit activeRulesCountChanged(); emit activeRulesCountChanged();
@@ -881,7 +929,7 @@ void ChatRootView::refreshRules()
} }
auto ruleFiles auto ruleFiles
= LLMCore::RulesLoader::getRuleFilesForProject(project, LLMCore::RulesContext::Chat); = PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
for (const auto &ruleFile : ruleFiles) { for (const auto &ruleFile : ruleFiles) {
QVariantMap ruleMap; QVariantMap ruleMap;
@@ -1232,9 +1280,9 @@ QString ChatRootView::lastInfoMessage() const
bool ChatRootView::isThinkingSupport() const bool ChatRootView::isThinkingSupport() const
{ {
auto providerName = Settings::generalSettings().caProvider(); auto providerName = Settings::generalSettings().caProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->supportThinking(); return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
} }
QString ChatRootView::generateChatFileName(const QString &shortMessage, const QString &dir) const QString ChatRootView::generateChatFileName(const QString &shortMessage, const QString &dir) const
@@ -1336,8 +1384,6 @@ void ChatRootView::applyConfiguration(const QString &configName)
settings.caModel.setValue(config.model); settings.caModel.setValue(config.model);
settings.caTemplate.setValue(config.templateName); settings.caTemplate.setValue(config.templateName);
settings.caUrl.setValue(config.url); settings.caUrl.setValue(config.url);
settings.caEndpointMode.setValue(
settings.caEndpointMode.indexForDisplay(config.endpointMode));
settings.caCustomEndpoint.setValue(config.customEndpoint); settings.caCustomEndpoint.setValue(config.customEndpoint);
settings.writeSettings(); settings.writeSettings();
@@ -1360,4 +1406,130 @@ QString ChatRootView::currentConfiguration() const
return m_currentConfiguration; return m_currentConfiguration;
} }
void ChatRootView::loadAvailableAgentRoles()
{
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
m_availableAgentRoles.clear();
m_availableAgentRoles.append(Settings::AgentRolesManager::getNoRole().name);
for (const auto &role : roles)
m_availableAgentRoles.append(role.name);
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
m_currentAgentRole = Settings::AgentRolesManager::getNoRole().name;
if (!lastRoleId.isEmpty()) {
for (const auto &role : roles) {
if (role.id == lastRoleId) {
m_currentAgentRole = role.name;
break;
}
}
}
emit availableAgentRolesChanged();
emit currentAgentRoleChanged();
}
void ChatRootView::applyAgentRole(const QString &roleName)
{
auto &settings = Settings::chatAssistantSettings();
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
settings.lastUsedRoleId.setValue("");
settings.writeSettings();
m_currentAgentRole = roleName;
emit currentAgentRoleChanged();
return;
}
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
for (const auto &role : roles) {
if (role.name == roleName) {
settings.lastUsedRoleId.setValue(role.id);
settings.writeSettings();
m_currentAgentRole = role.name;
emit currentAgentRoleChanged();
break;
}
}
}
QStringList ChatRootView::availableAgentRoles() const
{
return m_availableAgentRoles;
}
QString ChatRootView::currentAgentRole() const
{
return m_currentAgentRole;
}
QString ChatRootView::baseSystemPrompt() const
{
return Settings::chatAssistantSettings().systemPrompt();
}
QString ChatRootView::currentAgentRoleDescription() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
return role.description;
}
QString ChatRootView::currentAgentRoleSystemPrompt() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return QString();
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return QString();
return role.systemPrompt;
}
void ChatRootView::openAgentRolesSettings()
{
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
}
void ChatRootView::compressCurrentChat()
{
if (m_chatCompressor->isCompressing()) {
m_lastErrorMessage = tr("Compression is already in progress");
emit lastErrorMessageChanged();
return;
}
if (m_recentFilePath.isEmpty()) {
m_lastErrorMessage = tr("No chat file to compress. Please save the chat first.");
emit lastErrorMessageChanged();
return;
}
autosave();
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
}
void ChatRootView::cancelCompression()
{
m_chatCompressor->cancelCompression();
}
bool ChatRootView::isCompressing() const
{
return m_chatCompressor->isCompressing();
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,34 +1,21 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once #pragma once
#include <QQuickItem> #include <QQuickItem>
#include <QVariantList>
#include "ChatFileManager.hpp"
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "ChatFileManager.hpp" #include "pluginllmcore/PromptProviderChat.hpp"
#include "llmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
class ChatCompressor;
class ChatRootView : public QQuickItem class ChatRootView : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
@@ -59,6 +46,12 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
QML_ELEMENT QML_ELEMENT
@@ -96,6 +89,8 @@ public:
Q_INVOKABLE void openRulesFolder(); Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings(); Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath);
Q_INVOKABLE void updateInputTokensCount(); Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const; int inputTokensCount() const;
@@ -145,6 +140,18 @@ public:
Q_INVOKABLE void applyConfiguration(const QString &configName); Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const; QStringList availableConfigurations() const;
QString currentConfiguration() const; QString currentConfiguration() const;
Q_INVOKABLE void compressCurrentChat();
Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles();
Q_INVOKABLE void applyAgentRole(const QString &roleId);
Q_INVOKABLE void openAgentRolesSettings();
QStringList availableAgentRoles() const;
QString currentAgentRole() const;
QString baseSystemPrompt() const;
QString currentAgentRoleDescription() const;
QString currentAgentRoleSystemPrompt() const;
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
@@ -154,6 +161,8 @@ public:
QString lastInfoMessage() const; QString lastInfoMessage() const;
bool isThinkingSupport() const; bool isThinkingSupport() const;
bool isCompressing() const;
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message);
@@ -161,6 +170,7 @@ public slots:
void cancelRequest(); void cancelRequest();
void clearAttachmentFiles(); void clearAttachmentFiles();
void clearLinkedFiles(); void clearLinkedFiles();
void clearMessages();
signals: signals:
void chatModelChanged(); void chatModelChanged();
@@ -191,6 +201,16 @@ signals:
void availableConfigurationsChanged(); void availableConfigurationsChanged();
void currentConfigurationChanged(); void currentConfigurationChanged();
void availableAgentRolesChanged();
void currentAgentRoleChanged();
void baseSystemPromptChanged();
void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error);
void openFilesChanged();
private: private:
void updateFileEditStatus(const QString &editId, const QString &status); void updateFileEditStatus(const QString &editId, const QString &status);
QString getChatsHistoryDir() const; QString getChatsHistoryDir() const;
@@ -199,7 +219,7 @@ private:
bool hasImageAttachments(const QStringList &attachments) const; bool hasImageAttachments(const QStringList &attachments) const;
ChatModel *m_chatModel; ChatModel *m_chatModel;
LLMCore::PromptProviderChat m_promptProvider; PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager; ChatFileManager *m_fileManager;
QString m_currentTemplate; QString m_currentTemplate;
@@ -223,6 +243,11 @@ private:
QStringList m_availableConfigurations; QStringList m_availableConfigurations;
QString m_currentConfiguration; QString m_currentConfiguration;
QStringList m_availableAgentRoles;
QString m_currentAgentRole;
ChatCompressor *m_chatCompressor;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "Logger.hpp" #include "Logger.hpp"
@@ -38,14 +22,6 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {false, "Failed to create directory structure"}; return {false, "Failed to create directory structure"};
} }
QString contentFolder = getChatContentFolder(filePath);
QDir dir;
if (!dir.exists(contentFolder)) {
if (!dir.mkpath(contentFolder)) {
LOG_MESSAGE(QString("Warning: Failed to create content folder: %1").arg(contentFolder));
}
}
QFile file(filePath); QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)}; return {false, QString("Failed to open file for writing: %1").arg(filePath)};
@@ -88,21 +64,22 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
return {true, QString()}; return {true, QString()};
} }
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, const QString &chatFilePath) QJsonObject ChatSerializer::serializeMessage(
const ChatModel::Message &message, const QString &chatFilePath)
{ {
QJsonObject messageObj; QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role); messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content; messageObj["content"] = message.content;
messageObj["id"] = message.id; messageObj["id"] = message.id;
if (message.isRedacted) { if (message.isRedacted) {
messageObj["isRedacted"] = true; messageObj["isRedacted"] = true;
} }
if (!message.signature.isEmpty()) { if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature; messageObj["signature"] = message.signature;
} }
if (!message.attachments.isEmpty()) { if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray; QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) { for (const auto &attachment : message.attachments) {
@@ -113,7 +90,7 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
} }
messageObj["attachments"] = attachmentsArray; messageObj["attachments"] = attachmentsArray;
} }
if (!message.images.isEmpty()) { if (!message.images.isEmpty()) {
QJsonArray imagesArray; QJsonArray imagesArray;
for (const auto &image : message.images) { for (const auto &image : message.images) {
@@ -125,11 +102,12 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
} }
messageObj["images"] = imagesArray; messageObj["images"] = imagesArray;
} }
return messageObj; return messageObj;
} }
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, const QString &chatFilePath) ChatModel::Message ChatSerializer::deserializeMessage(
const QJsonObject &json, const QString &chatFilePath)
{ {
ChatModel::Message message; ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt()); message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
@@ -137,7 +115,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.id = json["id"].toString(); message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false); message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString(); message.signature = json["signature"].toString();
if (json.contains("attachments")) { if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray(); QJsonArray attachmentsArray = json["attachments"].toArray();
for (const auto &attachmentValue : attachmentsArray) { for (const auto &attachmentValue : attachmentsArray) {
@@ -148,7 +126,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.attachments.append(attachment); message.attachments.append(attachment);
} }
} }
if (json.contains("images")) { if (json.contains("images")) {
QJsonArray imagesArray = json["images"].toArray(); QJsonArray imagesArray = json["images"].toArray();
for (const auto &imageValue : imagesArray) { for (const auto &imageValue : imagesArray) {
@@ -160,7 +138,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.images.append(image); message.images.append(image);
} }
} }
return message; return message;
} }
@@ -178,7 +156,8 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString
return root; return root;
} }
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath) bool ChatSerializer::deserializeChat(
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
{ {
QJsonArray messagesArray = json["messages"].toArray(); QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages; QVector<ChatModel::Message> messages;
@@ -189,17 +168,24 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
} }
model->clear(); model->clear();
model->setLoadingFromHistory(true); model->setLoadingFromHistory(true);
for (const auto &message : messages) { for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id, message.attachments, message.images, message.isRedacted, message.signature); model->addMessage(
message.content,
message.role,
message.id,
message.attachments,
message.images,
message.isRedacted,
message.signature);
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3") LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
.arg(message.images.size()) .arg(message.images.size())
.arg(message.isRedacted) .arg(message.isRedacted)
.arg(message.signature.length())); .arg(message.signature.length()));
} }
model->setLoadingFromHistory(false); model->setLoadingFromHistory(false);
return true; return true;
@@ -217,12 +203,14 @@ bool ChatSerializer::validateVersion(const QString &version)
if (version == VERSION) { if (version == VERSION) {
return true; return true;
} }
if (version == "0.1") { if (version == "0.1") {
LOG_MESSAGE("Loading chat from old format 0.1 - images folder structure has changed from _images to _content"); LOG_MESSAGE(
"Loading chat from old format 0.1 - images folder structure has changed from _images "
"to _content");
return true; return true;
} }
return false; return false;
} }
@@ -234,10 +222,11 @@ QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
return QDir(dirPath).filePath(baseName + "_content"); return QDir(dirPath).filePath(baseName + "_content");
} }
bool ChatSerializer::saveContentToStorage(const QString &chatFilePath, bool ChatSerializer::saveContentToStorage(
const QString &fileName, const QString &chatFilePath,
const QString &base64Data, const QString &fileName,
QString &storedPath) const QString &base64Data,
QString &storedPath)
{ {
QString contentFolder = getChatContentFolder(chatFilePath); QString contentFolder = getChatContentFolder(chatFilePath);
QDir dir; QDir dir;
@@ -247,34 +236,34 @@ bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
return false; return false;
} }
} }
QFileInfo originalFileInfo(fileName); QFileInfo originalFileInfo(fileName);
QString extension = originalFileInfo.suffix(); QString extension = originalFileInfo.suffix();
QString baseName = originalFileInfo.completeBaseName(); QString baseName = originalFileInfo.completeBaseName();
QString uniqueName = QString("%1_%2.%3") QString uniqueName = QString("%1_%2.%3")
.arg(baseName) .arg(baseName)
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)) .arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
.arg(extension); .arg(extension);
QString fullPath = QDir(contentFolder).filePath(uniqueName); QString fullPath = QDir(contentFolder).filePath(uniqueName);
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8()); QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
QFile file(fullPath); QFile file(fullPath);
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath)); LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
return false; return false;
} }
if (file.write(contentData) == -1) { if (file.write(contentData) == -1) {
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString())); LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
return false; return false;
} }
file.close(); file.close();
storedPath = uniqueName; storedPath = uniqueName;
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath)); LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
return true; return true;
} }
@@ -282,16 +271,16 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
{ {
QString contentFolder = getChatContentFolder(chatFilePath); QString contentFolder = getChatContentFolder(chatFilePath);
QString fullPath = QDir(contentFolder).filePath(storedPath); QString fullPath = QDir(contentFolder).filePath(storedPath);
QFile file(fullPath); QFile file(fullPath);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath)); LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
return QString(); return QString();
} }
QByteArray contentData = file.readAll(); QByteArray contentData = file.readAll();
file.close(); file.close();
return contentData.toBase64(); return contentData.toBase64();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,10 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <projectexplorer/buildconfiguration.h> #include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/target.h> #include <projectexplorer/target.h>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
@@ -40,12 +26,15 @@
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <LLMQore/ToolsManager.hpp>
#include "tools/TodoTool.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp" #include "ProvidersManager.hpp"
#include "RequestConfig.hpp"
#include "ToolsSettings.hpp" #include "ToolsSettings.hpp"
#include <RulesLoader.hpp> #include <RulesLoader.hpp>
#include <context/ChangesManager.h> #include <context/ChangesManager.h>
@@ -53,7 +42,7 @@
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( ClientInterface::ClientInterface(
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent) ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent) : QObject(parent)
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_promptProvider(promptProvider) , m_promptProvider(promptProvider)
@@ -138,7 +127,7 @@ void ClientInterface::sendMessage(
auto &chatAssistantSettings = Settings::chatAssistantSettings(); auto &chatAssistantSettings = Settings::chatAssistantSettings();
auto providerName = Settings::generalSettings().caProvider(); auto providerName = Settings::generalSettings().caProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider) { if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
@@ -153,14 +142,21 @@ void ClientInterface::sendMessage(
return; return;
} }
LLMCore::ContextData context; PluginLLMCore::ContextData context;
const bool isToolsEnabled = useTools; const bool isToolsEnabled = useTools;
if (chatAssistantSettings.useSystemPrompt()) { if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt(); QString systemPrompt = chatAssistantSettings.systemPrompt();
auto project = LLMCore::RulesLoader::getActiveProject(); const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
if (!lastRoleId.isEmpty()) {
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (!role.id.isEmpty())
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
}
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) { if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName()); systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
@@ -170,12 +166,12 @@ void ClientInterface::sendMessage(
if (auto target = project->activeTarget()) { if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) { if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt += QString("\n# Active Build directory: %1") systemPrompt += QString("\n# Active Build directory: %1")
.arg(buildConfig->buildDirectory().toUrlishString()); .arg(buildConfig->buildDirectory().toUrlishString());
} }
} }
QString projectRules QString projectRules
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat); = PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) { if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules; systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
@@ -190,13 +186,13 @@ void ClientInterface::sendMessage(
context.systemPrompt = systemPrompt; context.systemPrompt = systemPrompt;
} }
QVector<LLMCore::Message> messages; QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) { for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) { if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
continue; continue;
} }
LLMCore::Message apiMessage; PluginLLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant"; apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content; apiMessage.content = msg.content;
@@ -216,7 +212,8 @@ void ClientInterface::sendMessage(
apiMessage.isRedacted = msg.isRedacted; apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature; 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); auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) { if (!apiImages.isEmpty()) {
apiMessage.images = apiImages; apiMessage.images = apiImages;
@@ -226,109 +223,105 @@ void ClientInterface::sendMessage(
messages.append(apiMessage); 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") LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size()))); .arg(provider->name(), QString::number(imageFiles.size())));
} }
context.history = messages; context.history = messages;
LLMCore::LLMConfig config; QJsonObject payload{
config.requestType = LLMCore::RequestType::Chat; {"model", Settings::generalSettings().caModel()}, {"stream", true}};
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}};
}
config.apiKey = provider->apiKey(); provider->prepareRequest(
payload,
config.provider->prepareRequest(
config.providerRequest,
promptTemplate, promptTemplate,
context, context,
LLMCore::RequestType::Chat, PluginLLMCore::RequestType::Chat,
useTools, useTools,
useThinking); useThinking);
QString requestId = QUuid::createUuid().toString(); provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
connect(
provider->client(),
&::LLMQore::BaseClient::chunkReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolResultReady,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}}; QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider}; m_activeRequests[requestId] = {request, provider};
emit requestStarted(requestId); emit requestStarted(requestId);
connect( if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
provider, && provider->toolsManager()) {
&LLMCore::Provider::partialResponseReceived, if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
this, provider->toolsManager()->tool("todo_tool"))) {
&ClientInterface::handlePartialResponse, todoTool->setCurrentSessionId(m_chatFilePath);
Qt::UniqueConnection); }
connect( }
provider,
&LLMCore::Provider::fullResponseReceived,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionCompleted,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::continuationStarted,
this,
&ClientInterface::handleCleanAccumulatedData,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::redactedThinkingBlockReceived,
this,
&ClientInterface::handleRedactedThinkingBlockReceived,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
} }
void ClientInterface::clearMessages() void ClientInterface::clearMessages()
{ {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider && !m_chatFilePath.isEmpty()
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
m_chatModel->clear(); m_chatModel->clear();
LOG_MESSAGE("Chat history cleared");
} }
void ClientInterface::cancelRequest() void ClientInterface::cancelRequest()
{ {
QSet<LLMCore::Provider *> providers; QSet<PluginLLMCore::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) { if (it.value().provider) {
providers.insert(it.value().provider); providers.insert(it.value().provider);
@@ -336,7 +329,7 @@ void ClientInterface::cancelRequest()
} }
for (auto *provider : providers) { 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) { for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
@@ -348,6 +341,7 @@ void ClientInterface::cancelRequest()
m_activeRequests.clear(); m_activeRequests.clear();
m_accumulatedResponses.clear(); m_accumulatedResponses.clear();
m_awaitingContinuation.clear();
LOG_MESSAGE("All requests cancelled and state cleared"); LOG_MESSAGE("All requests cancelled and state cleared");
} }
@@ -414,6 +408,12 @@ void ClientInterface::handlePartialResponse(const QString &requestId, const QStr
if (it == m_activeRequests.end()) if (it == m_activeRequests.end())
return; 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; m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value(); const RequestContext &ctx = it.value();
@@ -444,12 +444,9 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
+ ": " + finalText); + ": " + finalText);
emit messageReceivedCompletely(); emit messageReceivedCompletely();
if (it != m_activeRequests.end()) { m_activeRequests.erase(it);
m_activeRequests.erase(it); m_accumulatedResponses.remove(requestId);
} m_awaitingContinuation.remove(requestId);
if (m_accumulatedResponses.contains(requestId)) {
m_accumulatedResponses.remove(requestId);
}
} }
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error) void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
@@ -461,18 +458,9 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
emit errorOccurred(error); emit errorOccurred(error);
if (it != m_activeRequests.end()) { m_activeRequests.erase(it);
m_activeRequests.erase(it); m_accumulatedResponses.remove(requestId);
} m_awaitingContinuation.remove(requestId);
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));
} }
void ClientInterface::handleThinkingBlockReceived( void ClientInterface::handleThinkingBlockReceived(
@@ -483,19 +471,17 @@ void ClientInterface::handleThinkingBlockReceived(
return; return;
} }
m_chatModel->addThinkingBlock(requestId, thinking, signature); if (m_awaitingContinuation.remove(requestId)) {
} m_accumulatedResponses[requestId].clear();
void ClientInterface::handleRedactedThinkingBlockReceived(
const QString &requestId, const QString &signature)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE( LOG_MESSAGE(
QString("Ignoring redacted thinking block for non-chat request: %1").arg(requestId)); QString("Cleared accumulated responses for continuation request %1").arg(requestId));
return;
} }
m_chatModel->addRedactedThinkingBlock(requestId, signature); if (thinking.isEmpty()) {
m_chatModel->addRedactedThinkingBlock(requestId, signature);
} else {
m_chatModel->addThinkingBlock(requestId, thinking, signature);
}
} }
void ClientInterface::handleToolExecutionStarted( void ClientInterface::handleToolExecutionStarted(
@@ -507,6 +493,7 @@ void ClientInterface::handleToolExecutionStarted(
} }
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName); m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
m_awaitingContinuation.insert(requestId);
} }
void ClientInterface::handleToolExecutionCompleted( void ClientInterface::handleToolExecutionCompleted(
@@ -570,10 +557,10 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
return imageData.toBase64(); return imageData.toBase64();
} }
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage( QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const const QList<ChatModel::ImageAttachment> &storedImages) const
{ {
QVector<LLMCore::ImageAttachment> apiImages; QVector<PluginLLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) { for (const auto &storedImage : storedImages) {
QString base64Data QString base64Data
@@ -583,7 +570,7 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
continue; continue;
} }
LLMCore::ImageAttachment apiImage; PluginLLMCore::ImageAttachment apiImage;
apiImage.data = base64Data; apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType; apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false; apiImage.isUrl = false;
@@ -596,6 +583,20 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
void ClientInterface::setChatFilePath(const QString &filePath) void ClientInterface::setChatFilePath(const QString &filePath)
{ {
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
}
m_chatFilePath = filePath; m_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath); m_chatModel->setChatFilePath(filePath);
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,426 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "FileMentionItem.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
namespace QodeAssist::Chat {
FileMentionItem::FileMentionItem(QQuickItem *parent)
: QQuickItem(parent)
{}
QVariantList FileMentionItem::searchResults() const
{
return m_searchResults;
}
int FileMentionItem::currentIndex() const
{
return m_currentIndex;
}
void FileMentionItem::setCurrentIndex(int index)
{
if (m_currentIndex == index)
return;
m_currentIndex = index;
emit currentIndexChanged();
}
void FileMentionItem::updateSearch(const QString &query)
{
m_lastQuery = query;
QVariantList openFiles = getOpenFiles(query);
QVariantList projectResults = searchProjectFiles(query);
QSet<QString> openPaths;
for (const QVariant &item : std::as_const(openFiles)) {
const QVariantMap map = item.toMap();
openPaths.insert(map.value("absolutePath").toString());
}
QVariantList combined = openFiles;
for (const QVariant &item : std::as_const(projectResults)) {
const QVariantMap map = item.toMap();
if (!map.value("isProject").toBool()
&& openPaths.contains(map.value("absolutePath").toString()))
continue;
combined.append(item);
}
m_searchResults = combined;
m_currentIndex = 0;
emit searchResultsChanged();
emit currentIndexChanged();
}
void FileMentionItem::refreshSearch()
{
if (!m_lastQuery.isNull())
updateSearch(m_lastQuery);
}
void FileMentionItem::moveUp()
{
if (m_currentIndex > 0) {
m_currentIndex--;
emit currentIndexChanged();
}
}
void FileMentionItem::moveDown()
{
if (m_currentIndex < m_searchResults.size() - 1) {
m_currentIndex++;
emit currentIndexChanged();
}
}
void FileMentionItem::selectCurrent()
{
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
return;
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
if (item.value("isProject").toBool()) {
emit projectSelected(item.value("projectName").toString());
} else {
emit fileSelected(
item.value("absolutePath").toString(),
item.value("relativePath").toString(),
item.value("projectName").toString());
}
}
void FileMentionItem::dismiss()
{
m_searchResults.clear();
m_currentIndex = 0;
emit searchResultsChanged();
emit currentIndexChanged();
emit dismissed();
}
QVariantMap FileMentionItem::applyCurrentSelection(
const QString &text, int cursorPosition, bool useTools)
{
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size()) {
dismiss();
return {};
}
const QString textBefore = text.left(cursorPosition);
const int atIndex = textBefore.lastIndexOf('@');
if (atIndex < 0) {
dismiss();
return {};
}
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
QString replacement;
if (item.value("isProject").toBool()) {
replacement = QStringLiteral("@") + item.value("projectName").toString() + ":";
} else {
const QString currentQuery = textBefore.mid(atIndex + 1);
const QVariantMap result = handleFileSelection(
item.value("absolutePath").toString(),
item.value("relativePath").toString(),
item.value("projectName").toString(),
currentQuery,
useTools);
if (result.value("mode").toString() == "mention")
replacement = result.value("mentionText").toString();
}
const QString newText = text.left(atIndex) + replacement + text.mid(cursorPosition);
const int newCursorPosition = atIndex + replacement.length();
dismiss();
return {{"text", newText}, {"cursorPosition", newCursorPosition}};
}
QVariantMap FileMentionItem::handleFileSelection(
const QString &absolutePath,
const QString &relativePath,
const QString &projectName,
const QString &currentQuery,
bool useTools)
{
QVariantMap result;
const QString fileName = relativePath.section('/', -1);
QString mentionKey = fileName;
const int colonIdx = currentQuery.indexOf(':');
if (colonIdx > 0) {
const QString projPrefix = currentQuery.left(colonIdx);
if (projPrefix.compare(projectName, Qt::CaseInsensitive) == 0)
mentionKey = projPrefix + ":" + fileName;
}
if (useTools) {
registerMention(mentionKey, absolutePath);
result["mode"] = QStringLiteral("mention");
result["mentionText"] = "@" + mentionKey + " ";
} else {
emit fileAttachRequested({absolutePath});
result["mode"] = QStringLiteral("attach");
}
return result;
}
void FileMentionItem::registerMention(const QString &mentionKey, const QString &absolutePath)
{
m_atMentionMap[mentionKey] = absolutePath;
}
void FileMentionItem::clearMentions()
{
m_atMentionMap.clear();
}
QString FileMentionItem::expandMentions(const QString &text)
{
QString result = text;
for (auto it = m_atMentionMap.constBegin(); it != m_atMentionMap.constEnd(); ++it) {
const QString &mentionKey = it.key();
const QString &absPath = it.value();
const QString displayName = mentionKey.section(':', -1);
const QString escaped = QRegularExpression::escape(mentionKey);
// @key:N-M -> hyperlink + inline code block
const QRegularExpression rangeRe("@" + escaped + ":(\\d+)-(\\d+)(?=\\s|$)");
QRegularExpressionMatchIterator matchIt = rangeRe.globalMatch(result);
QList<QRegularExpressionMatch> matches;
while (matchIt.hasNext())
matches.append(matchIt.next());
for (int i = matches.size() - 1; i >= 0; --i) {
const auto &m = matches[i];
const int startLine = m.captured(1).toInt();
const int endLine = m.captured(2).toInt();
const QString ext = fileExtension(absPath);
const QString snippet = readFileLines(absPath, startLine, endLine);
const QString replacement
= QString("[@%1:%2-%3](file://%4)\n```%5\n%6```")
.arg(displayName)
.arg(startLine)
.arg(endLine)
.arg(absPath, ext, snippet);
result.replace(m.capturedStart(), m.capturedLength(), replacement);
}
// @key -> hyperlink only
const QRegularExpression simpleRe("@" + escaped + "(?=\\s|$)");
result.replace(simpleRe, QString("[@%1](file://%2)").arg(displayName, absPath));
}
return result;
}
QVariantList FileMentionItem::searchProjectFiles(const QString &query)
{
QVariantList results;
struct FileResult
{
QString absolutePath;
QString relativePath;
QString projectName;
int priority;
};
const auto allProjects = ProjectExplorer::ProjectManager::projects();
QString projectFilter;
QString fileQuery = query;
const int colonIdx = query.indexOf(':');
if (colonIdx > 0) {
const QString prefix = query.left(colonIdx);
for (auto project : allProjects) {
if (project && project->displayName().compare(prefix, Qt::CaseInsensitive) == 0) {
projectFilter = project->displayName();
fileQuery = query.mid(colonIdx + 1);
break;
}
}
}
if (projectFilter.isEmpty() && colonIdx < 0) {
const QString lowerQ = query.toLower();
for (auto project : allProjects) {
if (!project)
continue;
const QString name = project->displayName();
if (query.isEmpty() || name.toLower().startsWith(lowerQ)) {
QVariantMap item;
item["absolutePath"] = QString();
item["relativePath"] = name;
item["projectName"] = name;
item["isProject"] = true;
results.append(item);
}
}
}
QList<FileResult> candidates;
const QString lowerFileQuery = fileQuery.toLower();
const bool emptyFileQuery = fileQuery.isEmpty();
for (auto project : allProjects) {
if (!project)
continue;
if (!projectFilter.isEmpty() && project->displayName() != projectFilter)
continue;
const auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
const QString projectDir = project->projectDirectory().path();
const QString projectName = project->displayName();
for (const auto &filePath : projectFiles) {
const QString absolutePath = filePath.path();
const QFileInfo fileInfo(absolutePath);
const QString fileName = fileInfo.fileName();
const QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
const QString lowerFileName = fileName.toLower();
const QString lowerRelativePath = relativePath.toLower();
int priority = -1;
if (emptyFileQuery) {
priority = 3;
} else if (lowerFileName == lowerFileQuery) {
priority = 0;
} else if (lowerFileName.startsWith(lowerFileQuery)) {
priority = 1;
} else if (lowerFileName.contains(lowerFileQuery)) {
priority = 2;
} else if (lowerRelativePath.contains(lowerFileQuery)) {
priority = 3;
}
if (priority >= 0)
candidates.append({absolutePath, relativePath, projectName, priority});
}
}
std::sort(candidates.begin(), candidates.end(), [](const FileResult &a, const FileResult &b) {
if (a.priority != b.priority)
return a.priority < b.priority;
return a.relativePath < b.relativePath;
});
const int maxFiles = qMax(0, 10 - results.size());
const int count = qMin(candidates.size(), maxFiles);
for (int i = 0; i < count; i++) {
QVariantMap item;
item["absolutePath"] = candidates[i].absolutePath;
item["relativePath"] = candidates[i].relativePath;
item["projectName"] = candidates[i].projectName;
item["isProject"] = false;
results.append(item);
}
return results;
}
QVariantList FileMentionItem::getOpenFiles(const QString &query)
{
QVariantList results;
const QString lowerQuery = query.toLower();
const bool emptyQuery = query.isEmpty();
QSet<QString> addedPaths;
auto tryAddDocument = [&](Core::IDocument *document) {
if (!document)
return;
const QString absolutePath = document->filePath().toFSPathString();
if (absolutePath.isEmpty() || addedPaths.contains(absolutePath))
return;
const QFileInfo fileInfo(absolutePath);
const QString fileName = fileInfo.fileName();
if (fileName.isEmpty())
return;
QString relativePath = absolutePath;
QString projectName;
auto project = ProjectExplorer::ProjectManager::projectForFile(document->filePath());
if (project) {
projectName = project->displayName();
relativePath = QDir(project->projectDirectory().path()).relativeFilePath(absolutePath);
}
if (!emptyQuery) {
const QString lowerFileName = fileName.toLower();
const QString lowerRelativePath = relativePath.toLower();
if (!lowerFileName.contains(lowerQuery) && !lowerRelativePath.contains(lowerQuery))
return;
}
addedPaths.insert(absolutePath);
QVariantMap item;
item["absolutePath"] = absolutePath;
item["relativePath"] = relativePath;
item["projectName"] = projectName;
item["isProject"] = false;
item["isOpen"] = true;
results.append(item);
};
if (auto current = Core::EditorManager::currentEditor())
tryAddDocument(current->document());
for (auto editor : Core::EditorManager::visibleEditors())
if (editor)
tryAddDocument(editor->document());
for (auto document : Core::DocumentModel::openedDocuments())
tryAddDocument(document);
return results;
}
QString FileMentionItem::readFileLines(const QString &filePath, int startLine, int endLine)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
QTextStream stream(&file);
QString result;
int lineNum = 1;
while (!stream.atEnd()) {
const QString line = stream.readLine();
if (lineNum >= startLine)
result += line + '\n';
if (lineNum >= endLine)
break;
++lineNum;
}
return result;
}
QString FileMentionItem::fileExtension(const QString &filePath)
{
const int dot = filePath.lastIndexOf('.');
return dot >= 0 ? filePath.mid(dot + 1) : QString();
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,70 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QHash>
#include <QQuickItem>
#include <QRegularExpression>
#include <QVariantList>
namespace QodeAssist::Chat {
class FileMentionItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QVariantList searchResults READ searchResults NOTIFY searchResultsChanged FINAL)
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL)
QML_ELEMENT
public:
explicit FileMentionItem(QQuickItem *parent = nullptr);
QVariantList searchResults() const;
int currentIndex() const;
void setCurrentIndex(int index);
Q_INVOKABLE void updateSearch(const QString &query);
Q_INVOKABLE void refreshSearch();
Q_INVOKABLE void moveUp();
Q_INVOKABLE void moveDown();
Q_INVOKABLE void selectCurrent();
Q_INVOKABLE void dismiss();
Q_INVOKABLE QVariantMap handleFileSelection(
const QString &absolutePath,
const QString &relativePath,
const QString &projectName,
const QString &currentQuery,
bool useTools);
Q_INVOKABLE QVariantMap applyCurrentSelection(
const QString &text, int cursorPosition, bool useTools);
Q_INVOKABLE void registerMention(const QString &mentionKey, const QString &absolutePath);
Q_INVOKABLE void clearMentions();
Q_INVOKABLE QString expandMentions(const QString &text);
signals:
void searchResultsChanged();
void currentIndexChanged();
void fileSelected(const QString &absolutePath,
const QString &relativePath,
const QString &projectName);
void projectSelected(const QString &projectName);
void dismissed();
void fileAttachRequested(const QStringList &filePaths);
private:
QVariantList searchProjectFiles(const QString &query);
QVariantList getOpenFiles(const QString &query);
QString readFileLines(const QString &filePath, int startLine, int endLine);
static QString fileExtension(const QString &filePath);
QVariantList m_searchResults;
int m_currentIndex = 0;
QString m_lastQuery;
QHash<QString, QString> m_atMentionMap;
};
} // namespace QodeAssist::Chat

View File

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

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- Archive/compress icon: downward arrows pointing to center with horizontal lines -->
<line x1="12" y1="3" x2="12" y2="10" />
<polyline points="9 7 12 10 15 7" />
<line x1="12" y1="21" x2="12" y2="14" />
<polyline points="9 17 12 14 15 17" />
<line x1="4" y1="12" x2="20" y2="12" stroke-width="3" />
</svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -26,6 +10,7 @@ import UIControls
import Qt.labs.platform as Platform import Qt.labs.platform as Platform
import "./chatparts" import "./chatparts"
import "./controls"
ChatRootView { ChatRootView {
id: root id: root
@@ -77,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 { ColumnLayout {
id: mainColumn
anchors.fill: parent anchors.fill: parent
spacing: 0 spacing: 0
@@ -97,8 +97,7 @@ ChatRootView {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved") text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
} }
openChatHistory.onClicked: root.openChatHistoryFolder() openChatHistory.onClicked: root.openChatHistoryFolder()
rulesButton.onClicked: rulesViewer.open() contextButton.onClicked: contextViewer.open()
activeRulesCount: root.activeRulesCount
pinButton { pinButton {
visible: typeof _chatview !== 'undefined' visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
@@ -131,12 +130,24 @@ ChatRootView {
root.loadAvailableConfigurations() root.loadAvailableConfigurations()
} }
} }
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
}
}
} }
ListView { ListView {
id: chatListView id: chatListView
signal hideServiceComponents(int itemIndex) property bool userScrolledUp: false
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@@ -147,6 +158,18 @@ ChatRootView {
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000 cacheBuffer: 2000
onMovingChanged: {
if (moving) {
userScrolledUp = !atYEnd
}
}
onAtYEndChanged: {
if (atYEnd) {
userScrolledUp = false
}
}
delegate: Loader { delegate: Loader {
id: componentLoader id: componentLoader
@@ -167,11 +190,6 @@ ChatRootView {
} }
} }
onLoaded: {
if (componentLoader.sourceComponent == chatItemComponent) {
chatListView.hideServiceComponents(index)
}
}
} }
header: Item { header: Item {
@@ -183,12 +201,53 @@ ChatRootView {
id: scroll id: scroll
} }
Rectangle {
id: scrollToBottomButton
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
bottomMargin: 10
}
width: 36
height: 36
radius: 18
color: palette.button
border.color: palette.mid
border.width: 1
visible: chatListView.userScrolledUp
opacity: 0.9
z: 100
Text {
anchors.centerIn: parent
text: "▼"
font.pixelSize: 14
color: palette.buttonText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
chatListView.userScrolledUp = false
root.scrollToBottom()
}
}
Behavior on visible {
enabled: false
}
}
onCountChanged: { onCountChanged: {
root.scrollToBottom() if (!userScrolledUp) {
root.scrollToBottom()
}
} }
onContentHeightChanged: { onContentHeightChanged: {
if (atYEnd) { if (!userScrolledUp && atYEnd) {
root.scrollToBottom() root.scrollToBottom()
} }
} }
@@ -217,6 +276,10 @@ ChatRootView {
messageInput.cursorPosition = model.content.length messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx) root.chatModel.resetModelTo(idx)
} }
onOpenFileRequested: function(filePath) {
root.openFileInEditor(filePath)
}
} }
} }
@@ -224,19 +287,8 @@ ChatRootView {
id: toolMessageComponent id: toolMessageComponent
ToolBlock { ToolBlock {
id: toolsItem
width: parent.width width: parent.width
toolContent: model.content toolContent: model.content
Connections {
target: chatListView
function onHideServiceComponents(itemIndex) {
if (index !== itemIndex) {
toolsItem.headerOpacity = 0.5
}
}
}
} }
} }
@@ -269,8 +321,6 @@ ChatRootView {
id: thinkingMessageComponent id: thinkingMessageComponent
ThinkingBlock { ThinkingBlock {
id: thinking
width: parent.width width: parent.width
thinkingContent: { thinkingContent: {
let content = model.content let content = model.content
@@ -281,15 +331,6 @@ ChatRootView {
return content return content
} }
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
Connections {
target: chatListView
function onHideServiceComponents(itemIndex) {
if (index !== itemIndex) {
thinking.headerOpacity = 0.5
}
}
}
} }
} }
} }
@@ -327,7 +368,38 @@ ChatRootView {
} }
} }
onTextChanged: root.calculateMessageTokensCount(messageInput.text) onTextChanged: {
root.calculateMessageTokensCount(messageInput.text)
var cursorPos = messageInput.cursorPosition
var textBefore = messageInput.text.substring(0, cursorPos)
var atIndex = textBefore.lastIndexOf('@')
if (atIndex >= 0) {
var query = textBefore.substring(atIndex + 1)
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
fileMentionPopup.updateSearch(query)
return
}
}
fileMentionPopup.dismiss()
}
Keys.onPressed: function(event) {
if (fileMentionPopup.visible) {
if (event.key === Qt.Key_Down) {
fileMentionPopup.moveDown()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
fileMentionPopup.moveUp()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.applyMentionSelection()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
fileMentionPopup.dismiss()
event.accepted = true
}
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -417,12 +489,16 @@ ChatRootView {
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
Layout.preferredHeight: 40 Layout.preferredHeight: 40
isCompressing: root.isCompressing
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage() sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest() : root.cancelRequest()
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg" sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg" : "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return") sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
: qsTr("Stop") : qsTr("Stop")
compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles { syncOpenFiles {
checked: root.isSyncOpenFiles checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked) onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
@@ -438,15 +514,12 @@ ChatRootView {
sequences: ["Ctrl+Return", "Ctrl+Enter"] sequences: ["Ctrl+Return", "Ctrl+Enter"]
context: Qt.WindowShortcut context: Qt.WindowShortcut
onActivated: { enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
if (messageInput.activeFocus && !Qt.inputMethod.visible) { onActivated: root.sendChatMessage()
root.sendChatMessage()
}
}
} }
function clearChat() { function clearChat() {
root.chatModel.clear() root.clearMessages()
root.clearAttachmentFiles() root.clearAttachmentFiles()
root.updateInputTokensCount() root.updateInputTokensCount()
} }
@@ -455,12 +528,38 @@ ChatRootView {
Qt.callLater(chatListView.positionViewAtEnd) Qt.callLater(chatListView.positionViewAtEnd)
} }
function applyMentionSelection() {
var result = fileMentionPopup.applyCurrentSelection(
messageInput.text, messageInput.cursorPosition, root.useTools)
if (result.text !== undefined) {
messageInput.text = result.text
messageInput.cursorPosition = result.cursorPosition
}
}
function sendChatMessage() { function sendChatMessage() {
root.sendMessage(messageInput.text) root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = "" messageInput.text = ""
fileMentionPopup.clearMentions()
scrollToBottom() scrollToBottom()
} }
Dialog {
id: compressConfirmDialog
anchors.centerIn: parent
title: qsTr("Compress Chat")
modal: true
standardButtons: Dialog.Yes | Dialog.No
Label {
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
wrapMode: Text.WordWrap
}
onAccepted: root.compressCurrentChat()
}
Toast { Toast {
id: errorToast id: errorToast
z: 1000 z: 1000
@@ -479,19 +578,28 @@ ChatRootView {
toastTextColor: "#FFFFFF" toastTextColor: "#FFFFFF"
} }
RulesViewer { ContextViewer {
id: rulesViewer id: contextViewer
width: parent.width * 0.8 width: Math.min(parent.width * 0.85, 800)
height: parent.height * 0.8 height: Math.min(parent.height * 0.85, 700)
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 2 y: (parent.height - height) / 2
baseSystemPrompt: root.baseSystemPrompt
currentAgentRole: root.currentAgentRole
currentAgentRoleDescription: root.currentAgentRoleDescription
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
activeRules: root.activeRules activeRules: root.activeRules
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex) activeRulesCount: root.activeRulesCount
onRefreshRules: root.refreshRules() onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder() onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
} }
Connections { Connections {
@@ -506,6 +614,26 @@ ChatRootView {
infoToast.show(root.lastInfoMessage) infoToast.show(root.lastInfoMessage)
} }
} }
function onOpenFilesChanged() {
if (fileMentionPopup.visible)
Qt.callLater(fileMentionPopup.refreshSearch)
}
}
FileMentionPopup {
id: fileMentionPopup
z: 999
width: Math.min(480, root.width - 20)
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
y: view.y - height - 4
onSelectionRequested: root.applyMentionSelection()
onFileAttachRequested: function(filePaths) {
root.addFilesToAttachList(filePaths)
}
} }
Component.onCompleted: { Component.onCompleted: {

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick import QtQuick
import ChatView import ChatView
@@ -51,6 +35,7 @@ Rectangle {
property int messageIndex: -1 property int messageIndex: -1
signal resetChatToMessage(int index) signal resetChatToMessage(int index)
signal openFileRequested(string filePath)
height: msgColumn.implicitHeight + 10 height: msgColumn.implicitHeight + 10
radius: 8 radius: 8
@@ -180,9 +165,12 @@ Rectangle {
onClicked: function() { onClicked: function() {
root.resetChatToMessage(root.messageIndex) root.resetChatToMessage(root.messageIndex)
} }
ToolTip.visible: hovered
ToolTip.text: qsTr("Reset chat to this message and edit") QoAToolTip {
ToolTip.delay: 500 visible: stopButtonId.hovered
text: qsTr("Reset chat to this message and edit")
delay: 500
}
} }
component TextComponent : TextBlock { component TextComponent : TextBlock {
@@ -204,6 +192,15 @@ Rectangle {
} }
} }
onLinkActivated: function(link) {
if (link.startsWith("file://")) {
var filePath = link.replace(/^file:\/\//, "")
root.openFileRequested(filePath)
} else {
Qt.openUrlExternally(link)
}
}
ChatUtils { ChatUtils {
id: utils id: utils
} }
@@ -257,33 +254,21 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) { if (mouse.modifiers & Qt.ShiftModifier) {
fileItem.openFileInExternalEditor()
} else {
fileItem.openFileInEditor() fileItem.openFileInEditor()
} else if (mouse.button === Qt.RightButton) {
attachmentContextMenu.popup()
} }
} }
ToolTip.visible: containsMouse QoAToolTip {
ToolTip.text: qsTr("Left click: Open in Qt Creator\nRight click: More options") visible: attachFileMouseArea.containsMouse
ToolTip.delay: 500 text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
} delay: 500
Menu {
id: attachmentContextMenu
MenuItem {
text: qsTr("Open in Qt Creator")
onTriggered: fileItem.openFileInEditor()
}
MenuItem {
text: qsTr("Open in System Editor")
onTriggered: fileItem.openFileInExternalEditor()
} }
} }
} }
@@ -305,7 +290,7 @@ Rectangle {
FileItem { FileItem {
id: imageFileItem id: imageFileItem
filePath: itemData.imageUrl ? itemData.imageUrl.toString().replace("file://", "") : "" filePath: itemData.filePath || ""
} }
ColumnLayout { ColumnLayout {
@@ -361,33 +346,21 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) { if (mouse.modifiers & Qt.ShiftModifier) {
imageFileItem.openFileInExternalEditor()
} else {
imageFileItem.openFileInEditor() imageFileItem.openFileInEditor()
} else if (mouse.button === Qt.RightButton) {
imageContextMenu.popup()
} }
} }
ToolTip.visible: containsMouse QoAToolTip {
ToolTip.text: qsTr("Left click: Open in System\nRight click: More options") visible: imageMouseArea.containsMouse
ToolTip.delay: 500 text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
} delay: 500
Menu {
id: imageContextMenu
MenuItem {
text: qsTr("Open in Qt Creator")
onTriggered: imageFileItem.openFileInEditor()
}
MenuItem {
text: qsTr("Open in System Viewer")
onTriggered: imageFileItem.openFileInExternalEditor()
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,11 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ChatView import ChatView
import UIControls
Flow { Flow {
id: root id: root
@@ -78,9 +63,11 @@ Flow {
} }
} }
ToolTip.visible: containsMouse QoAToolTip {
ToolTip.delay: 500 visible: mouse.containsMouse
ToolTip.text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove" delay: 500
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
}
} }
Menu { Menu {

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -31,6 +15,10 @@ Rectangle {
property alias attachFiles: attachFilesId property alias attachFiles: attachFilesId
property alias attachImages: attachImagesId property alias attachImages: attachImagesId
property alias linkFiles: linkFilesId property alias linkFiles: linkFilesId
property alias compressButton: compressButtonId
property alias cancelCompressButton: cancelCompressButtonId
property bool isCompressing: false
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@@ -49,17 +37,6 @@ Rectangle {
spacing: 10 spacing: 10
QoAButton {
id: sendButtonId
icon {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
}
QoAButton { QoAButton {
id: attachFilesId id: attachFilesId
@@ -111,5 +88,66 @@ Rectangle {
Item { Item {
Layout.fillWidth: true 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")
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.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
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
}
QoAButton {
id: sendButtonId
icon {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
}
} }
} }

View File

@@ -0,0 +1,542 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property string baseSystemPrompt
property string currentAgentRole
property string currentAgentRoleDescription
property string currentAgentRoleSystemPrompt
property var activeRules
property int activeRulesCount
property string selectedRuleContent
signal openSettings()
signal openAgentRolesSettings()
signal openRulesFolder()
signal refreshRules()
signal ruleSelected(int index)
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Chat Context")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Flickable {
id: mainFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: sectionsColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: sectionsColumn
width: mainFlickable.width
spacing: 8
CollapsibleSection {
id: systemPromptSection
Layout.fillWidth: true
title: qsTr("Base System Prompt")
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 5
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
Flickable {
id: systemPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: systemPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: systemPromptText
width: systemPromptFlickable.width
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.baseSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
}
QoAButton {
text: qsTr("Edit in Settings")
onClicked: {
root.openSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: agentRoleSection
Layout.fillWidth: true
title: qsTr("Agent Role")
badge: root.currentAgentRole
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
Text {
text: root.currentAgentRoleDescription
font.pixelSize: 11
font.italic: true
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleDescription.length > 0
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
visible: root.currentAgentRoleSystemPrompt.length > 0
Flickable {
id: agentPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: agentPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: agentPromptText
width: agentPromptFlickable.width
text: root.currentAgentRoleSystemPrompt
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
Text {
text: qsTr("No role selected. Using base system prompt only.")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleSystemPrompt.length === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.currentAgentRoleSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
}
QoAButton {
text: qsTr("Manage Roles")
onClicked: {
root.openAgentRolesSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: projectRulesSection
Layout.fillWidth: true
title: qsTr("Project Rules")
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
SplitView {
Layout.fillWidth: true
Layout.preferredHeight: 220
orientation: Qt.Horizontal
visible: root.activeRulesCount > 0
Rectangle {
SplitView.minimumWidth: 120
SplitView.preferredWidth: 180
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules (%1)").arg(rulesList.count)
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
boundsBehavior: Flickable.StopAtBounds
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
height: ruleItemContent.implicitHeight + 8
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted)
return palette.highlight
if (parent.hovered)
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
id: ruleItemContent
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 10
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: modelData.category
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
root.ruleSelected(index)
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
Rectangle {
SplitView.fillWidth: true
SplitView.minimumWidth: 200
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: root.selectedRuleContent.length > 0
onClicked: utils.copyToClipboard(root.selectedRuleContent)
}
}
Flickable {
id: ruleContentFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: ruleContentArea.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: ruleContentArea
width: ruleContentFlickable.width
text: root.selectedRuleContent
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
}
Text {
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
visible: root.activeRulesCount === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Open Rules Folder")
onClicked: root.openRulesFolder()
}
}
}
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Text {
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
component CollapsibleSection: ColumnLayout {
id: sectionRoot
property string title
property string badge
property color badgeColor: palette.mid
property Component sectionContent: null
property bool expanded: false
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
border.color: palette.mid
border.width: 1
radius: 2
MouseArea {
id: sectionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: sectionRoot.expanded = !sectionRoot.expanded
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
text: sectionRoot.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.text
}
Text {
text: sectionRoot.title
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
Rectangle {
implicitWidth: badgeText.implicitWidth + 12
implicitHeight: 18
color: sectionRoot.badgeColor
radius: 3
Text {
id: badgeText
anchors.centerIn: parent
text: sectionRoot.badge
font.pixelSize: 10
color: "#FFFFFF"
}
}
}
}
Loader {
id: contentLoader
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 8
Layout.bottomMargin: 4
sourceComponent: sectionRoot.sectionContent
visible: sectionRoot.expanded
active: sectionRoot.expanded
}
}
onOpened: {
if (root.activeRulesCount > 0) {
root.ruleSelected(0)
}
}
}

View File

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

View File

@@ -0,0 +1,151 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FileMentionItem {
id: root
signal selectionRequested()
visible: searchResults.length > 0
height: Math.min(searchResults.length * 36, 36 * 6) + 2
onCurrentIndexChanged: {
listView.positionViewAtIndex(root.currentIndex, ListView.Contain)
}
Rectangle {
id: background
anchors.fill: parent
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ListView {
id: listView
anchors.fill: parent
anchors.margins: 1
model: root.searchResults
currentIndex: root.currentIndex
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
id: delegateItem
required property int index
required property var modelData
readonly property bool isProject: modelData.isProject === true
readonly property bool isOpen: modelData.isOpen === true
readonly property string fileName: {
if (isProject)
return modelData.projectName
const parts = modelData.relativePath.split('/')
return parts[parts.length - 1]
}
width: listView.width
height: 36
color: index === root.currentIndex
? palette.highlight
: (hoverArea.containsMouse
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
: "transparent")
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
Item {
Layout.preferredWidth: 18
Layout.preferredHeight: 18
Rectangle {
anchors.fill: parent
radius: 3
visible: delegateItem.isProject || delegateItem.isOpen
color: {
if (delegateItem.index === root.currentIndex)
return Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.2)
if (delegateItem.isProject)
return Qt.rgba(palette.highlight.r,
palette.highlight.g,
palette.highlight.b, 0.3)
return Qt.rgba(0.2, 0.7, 0.4, 0.3)
}
Text {
anchors.centerIn: parent
text: delegateItem.isProject ? "P" : "O"
font.bold: true
font.pixelSize: 10
color: {
if (delegateItem.index === root.currentIndex)
return palette.highlightedText
if (delegateItem.isProject)
return palette.highlight
return Qt.rgba(0.1, 0.6, 0.3, 1.0)
}
}
}
}
Text {
Layout.preferredWidth: 160
text: delegateItem.fileName
color: delegateItem.index === root.currentIndex
? palette.highlightedText
: (delegateItem.isProject ? palette.highlight : palette.text)
font.bold: true
font.italic: delegateItem.isProject
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: delegateItem.isProject
? "→"
: (delegateItem.modelData.projectName + " / " + delegateItem.modelData.relativePath)
color: delegateItem.index === root.currentIndex
? (delegateItem.isProject
? palette.highlightedText
: Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.7))
: palette.mid
font.pixelSize: delegateItem.isProject ? 12 : 11
elide: Text.ElideLeft
horizontalAlignment: delegateItem.isProject ? Text.AlignLeft : Text.AlignRight
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.currentIndex = delegateItem.index
root.selectionRequested()
}
onEntered: root.currentIndex = delegateItem.index
}
}
}
}

View File

@@ -1,251 +0,0 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property var activeRules
property alias rulesCurrentIndex: rulesList.currentIndex
property alias ruleContentAreaText: ruleContentArea.text
signal refreshRules()
signal openRulesFolder()
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Active Project Rules")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Open Folder")
onClicked: root.openRulesFolder()
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
SplitView {
Layout.fillWidth: true
Layout.fillHeight: true
orientation: Qt.Horizontal
Rectangle {
SplitView.minimumWidth: 200
SplitView.preferredWidth: parent.width * 0.3
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules Files (%1)").arg(rulesList.count)
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted) {
return palette.highlight
} else if (parent.hovered) {
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
}
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 11
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: qsTr("Category: %1").arg(modelData.category)
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
}
}
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
}
Text {
visible: rulesList.count === 0
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
font.pixelSize: 10
color: palette.mid
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignCenter
}
}
}
Rectangle {
SplitView.fillWidth: true
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: ruleContentArea.text.length > 0
onClicked: utils.copyToClipboard(ruleContentArea.text)
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
TextEdit {
id: ruleContentArea
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
}
}
}
}
Text {
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}

View File

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

View File

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

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
@@ -33,12 +17,12 @@ Rectangle {
property alias recentPath: recentPathId property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId property alias pinButton: pinButtonId
property alias rulesButton: rulesButtonId property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId property alias settingsButton: settingsButtonId
property alias activeRulesCount: activeRulesCountId.text
property alias configSelector: configSelectorId property alias configSelector: configSelectorId
property alias roleSelector: roleSelector
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@@ -87,9 +71,26 @@ Rectangle {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: qsTr("Switch AI configuration") ToolTip.text: qsTr("Switch saved AI configuration")
} }
QoAComboBox {
id: roleSelector
implicitHeight: 25
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch agent role (different system prompts)")
}
}
Row {
spacing: 10
QoAButton { QoAButton {
id: toolsButtonId id: toolsButtonId
@@ -157,8 +158,13 @@ Rectangle {
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: qsTr("Open Chat Assistant Settings") ToolTip.text: qsTr("Open Chat Assistant Settings")
} }
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
} }
Item { Item {
height: firstRow.height height: firstRow.height
width: recentPathId.width width: recentPathId.width
@@ -217,19 +223,6 @@ Rectangle {
ToolTip.text: qsTr("Load chat from *.json file") ToolTip.text: qsTr("Load chat from *.json file")
} }
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
QoAButton { QoAButton {
id: openChatHistoryId id: openChatHistoryId
@@ -243,36 +236,21 @@ Rectangle {
ToolTip.text: qsTr("Show in system") ToolTip.text: qsTr("Show in system")
} }
QoASeparator {}
QoAButton { QoAButton {
id: rulesButtonId id: contextButtonId
icon { icon {
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg" source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15 height: 15
width: 15 width: 15
} }
text: " "
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: root.activeRulesCount > 0 ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
: qsTr("View active project rules (no rules found)")
Text {
id: activeRulesCountId
anchors {
bottom: parent.bottom
bottomMargin: 2
right: parent.right
rightMargin: 4
}
color: palette.text
font.pixelSize: 10
font.bold: true
}
} }
Badge { Badge {
@@ -282,6 +260,21 @@ Rectangle {
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold") ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
} }
QoASeparator {}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
} }
} }
} }

View File

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

View File

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

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2025-2026 Petr Mironychev
* Copyright (C) 2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
@@ -41,7 +25,7 @@ void ConfigurationManager::init()
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect) 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) { if (!templ) {
return; return;
@@ -65,7 +49,7 @@ void ConfigurationManager::updateAllTemplateDescriptions()
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect) 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()) if (templ->name() == templateAspect.value())
return; return;
@@ -86,8 +70,8 @@ void ConfigurationManager::checkAllTemplate()
ConfigurationManager::ConfigurationManager(QObject *parent) ConfigurationManager::ConfigurationManager(QObject *parent)
: QObject(parent) : QObject(parent)
, m_generalSettings(Settings::generalSettings()) , m_generalSettings(Settings::generalSettings())
, m_providersManager(LLMCore::ProvidersManager::instance()) , m_providersManager(PluginLLMCore::ProvidersManager::instance())
, m_templateManger(LLMCore::PromptTemplateManager::instance()) , m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
{} {}
void ConfigurationManager::setupConnections() void ConfigurationManager::setupConnections()
@@ -170,28 +154,26 @@ void ConfigurationManager::selectModel()
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue() : isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
: m_generalSettings.caUrl.volatileValue(); : m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model : isPreset1 ? m_generalSettings.ccPreset1Model
: isQuickRefactor ? m_generalSettings.qrModel : isQuickRefactor ? m_generalSettings.qrModel
: m_generalSettings.caModel; : m_generalSettings.caModel);
if (auto provider = m_providersManager.getProviderByName(providerName)) { if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) { if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
m_generalSettings.showModelsNotSupportedDialog(targetSettings); m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
return; return;
} }
const auto modelList = provider->getInstalledModels(providerUrl); provider->getInstalledModels(providerUrl)
.then(this, [this, targetSettings](const QList<QString> &modelList) {
if (modelList.isEmpty()) { if (modelList.isEmpty()) {
m_generalSettings.showModelsNotFoundDialog(targetSettings); m_generalSettings.showModelsNotFoundDialog(*targetSettings);
return; return;
} }
m_generalSettings.showSelectionDialog(
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() { modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
m_generalSettings.showSelectionDialog( });
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
});
} }
} }

View File

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

View File

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

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once #pragma once
@@ -25,9 +9,9 @@
#include <context/ContextManager.hpp> #include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp> #include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp> #include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp> #include <pluginllmcore/ContextData.hpp>
#include <llmcore/IPromptProvider.hpp> #include <pluginllmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp> #include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp> #include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp> #include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp> #include <settings/GeneralSettings.hpp>
@@ -45,8 +29,8 @@ public:
LLMClientInterface( LLMClientInterface(
const Settings::GeneralSettings &generalSettings, const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings, const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry, PluginLLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider, PluginLLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader, Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger); IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override; ~LLMClientInterface() override;
@@ -82,17 +66,19 @@ private:
struct RequestContext struct RequestContext
{ {
QJsonObject originalRequest; QJsonObject originalRequest;
LLMCore::Provider *provider; PluginLLMCore::Provider *provider;
}; };
LLMCore::ContextData prepareContext( PluginLLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); 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::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings; const Settings::GeneralSettings &m_generalSettings;
LLMCore::IPromptProvider *m_promptProvider = nullptr; PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
LLMCore::IProviderRegistry &m_providerRegistry; PluginLLMCore::IProviderRegistry &m_providerRegistry;
Context::IDocumentReader &m_documentReader; Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger; IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;

View File

@@ -1,26 +1,6 @@
/* // Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* 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/>.
*/
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
@@ -29,56 +9,43 @@
namespace QodeAssist { namespace QodeAssist {
static QStringList extractTokens(const QString &str) static bool isClosingTail(const QString &s, int from)
{ {
QStringList tokens; static const QString closeChars = QStringLiteral("(){}[];,");
QString currentToken; for (int i = from; i < s.size(); ++i) {
for (const QChar &ch : str) { const QChar c = s.at(i);
if (ch.isLetterOrNumber() || ch == '_') { if (!c.isSpace() && !closeChars.contains(c))
currentToken += ch; return false;
} else {
if (!currentToken.isEmpty() && currentToken.length() > 1) {
tokens.append(currentToken);
}
currentToken.clear();
}
} }
if (!currentToken.isEmpty() && currentToken.length() > 1) { return true;
tokens.append(currentToken);
}
return tokens;
} }
int LLMSuggestion::calculateReplaceLength(const QString &suggestion, int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText)
const QString &rightText,
const QString &entireLine)
{ {
if (rightText.isEmpty()) { if (rightText.isEmpty())
return 0; 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 = "{}[]()<>;,"; if (!isClosingTail(rightText, 0))
bool hasStructuralOverlap = false; return 0;
for (const QChar &ch : structuralChars) {
if (suggestion.contains(ch) && rightText.contains(ch)) { static const QString closeChars = QStringLiteral("(){}[];,");
hasStructuralOverlap = true; int i = suggestion.size() - 1;
break; while (i >= 0 && suggestion.at(i).isSpace())
} --i;
} if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i)))
return rightText.size();
if (hasStructuralOverlap) {
return rightText.length();
}
const QStringList suggestionTokens = extractTokens(suggestion);
const QStringList lineTokens = extractTokens(entireLine);
for (const auto &token : suggestionTokens) {
if (lineTokens.contains(token)) {
return rightText.length();
}
}
return 0; return 0;
} }
@@ -102,22 +69,21 @@ LLMSuggestion::LLMSuggestion(
QString rightText = blockText.mid(cursorPositionInBlock); QString rightText = blockText.mid(cursorPositionInBlock);
QString suggestionText = data.text; QString suggestionText = data.text;
QString entireLine = blockText;
if (!suggestionText.contains('\n')) { if (!suggestionText.contains('\n')) {
int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine); int replaceLength = calculateReplaceLength(suggestionText, rightText);
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText; QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
QString displayText = leftText + suggestionText + remainingRightText; QString displayText = leftText + suggestionText + remainingRightText;
replacementDocument()->setPlainText(displayText); replacementDocument()->setPlainText(displayText);
} else { } else {
int firstLineEnd = suggestionText.indexOf('\n'); int firstLineEnd = suggestionText.indexOf('\n');
QString firstLine = suggestionText.left(firstLineEnd); QString firstLine = suggestionText.left(firstLineEnd);
QString restOfCompletion = suggestionText.mid(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 remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion; QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
replacementDocument()->setPlainText(displayText); replacementDocument()->setPlainText(displayText);
} }
@@ -167,10 +133,9 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
if (startPos == 0) { if (startPos == 0) {
QTextBlock currentBlock = cursor.block(); QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock()); QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QString entireLine = currentBlock.text();
int replaceLength = calculateReplaceLength(text, textAfterCursor);
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
if (replaceLength > 0) { if (replaceLength > 0) {
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
currentCursor.removeSelectedText(); currentCursor.removeSelectedText();
@@ -220,9 +185,7 @@ bool LLMSuggestion::apply()
QString text = currentData.text; QString text = currentData.text;
QTextBlock currentBlock = cursor.block(); QTextBlock currentBlock = cursor.block();
QString textBeforeCursor = currentBlock.text().left(cursor.positionInBlock());
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock()); QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QString entireLine = currentBlock.text();
QTextCursor editCursor = cursor; QTextCursor editCursor = cursor;
editCursor.beginEditBlock(); editCursor.beginEditBlock();
@@ -232,22 +195,22 @@ bool LLMSuggestion::apply()
QString firstLine = text.left(firstLineEnd); QString firstLine = text.left(firstLineEnd);
QString restOfText = text.mid(firstLineEnd); QString restOfText = text.mid(firstLineEnd);
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine); int replaceLength = calculateReplaceLength(firstLine, textAfterCursor);
if (replaceLength > 0) { if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText(); editCursor.removeSelectedText();
} }
editCursor.insertText(firstLine + restOfText); editCursor.insertText(firstLine + restOfText);
} else { } else {
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine); int replaceLength = calculateReplaceLength(text, textAfterCursor);
if (replaceLength > 0) { if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength); editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText(); editCursor.removeSelectedText();
} }
editCursor.insertText(text); editCursor.insertText(text);
} }

View File

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

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * 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. * This file is part of QodeAssist.
* *

View File

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

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * 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. * This file is part of QodeAssist.
* *
@@ -54,6 +54,90 @@ using namespace Core;
namespace QodeAssist { 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) QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface) : LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface) , m_llmClient(clientInterface)
@@ -69,10 +153,6 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
m_typingTimer.start(); 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_refactorHoverHandler = new RefactorSuggestionHoverHandler();
m_refactorWidgetHandler = new RefactorWidgetHandler(this); m_refactorWidgetHandler = new RefactorWidgetHandler(this);
} }
@@ -108,6 +188,9 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (!Settings::codeCompletionSettings().autoCompletion()) if (!Settings::codeCompletionSettings().autoCompletion())
return; return;
if (isManualMode())
return;
auto project = ProjectManager::projectForFile(document->filePath()); auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project)) if (!isEnabled(project))
return; return;
@@ -131,38 +214,29 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (charsRemoved > 0 || charsAdded <= 0) { if (charsRemoved > 0 || charsAdded <= 0) {
m_recentCharCount = 0; m_recentCharCount = 0;
m_typingTimer.restart(); m_typingTimer.restart();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
return; return;
} }
QTextCursor cursor = widget->textCursor(); QTextCursor cursor = widget->textCursor();
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); 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_recentCharCount = 0;
m_typingTimer.restart(); m_typingTimer.restart();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
return; return;
} }
bool isSpaceOrTab = lastChar[0].isSpace(); const bool isSpaceOrTab = lastCh.isSpace();
bool ignoreWhitespace const bool ignoreWhitespace
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount(); = Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
if (!ignoreWhitespace || !isSpaceOrTab) { if (!ignoreWhitespace || !isSpaceOrTab)
m_recentCharCount += charsAdded; m_recentCharCount += charsAdded;
}
if (m_typingTimer.elapsed() if (m_typingTimer.elapsed()
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) { > Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
@@ -170,13 +244,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
m_typingTimer.restart(); m_typingTimer.restart();
} }
// 0 = Hint-based, 1 = Automatic handleAutoRequestTrigger(widget);
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode == 1) {
handleAutoRequestTrigger(widget, charsAdded, isSpaceOrTab);
} else {
handleHintBasedTrigger(widget, charsAdded, isSpaceOrTab, cursor);
}
}); });
} }
@@ -205,11 +273,9 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible()) if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return; return;
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 0) {
editor->abortAssist(); editor->abortAssist();
}
const FilePath filePath = editor->textDocument()->filePath(); const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{ GetCompletionRequest request{
@@ -270,33 +336,29 @@ void QodeAssistClient::requestQuickRefactor(
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor) 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); auto it = m_scheduledRequests.find(editor);
if (it == m_scheduledRequests.end()) { if (it == m_scheduledRequests.end()) {
auto timer = new QTimer(this); auto timer = new QTimer(this);
timer->setSingleShot(true); timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this, editor]() { connect(timer, &QTimer::timeout, this, [this, editor]() {
if (editor if (!editor || m_runningRequests.contains(editor))
&& editor->textCursor().position() return;
== m_scheduledRequests[editor]->property("cursorPosition").toInt() if (editor->textCursor().position()
&& m_recentCharCount != m_scheduledRequests[editor]->property("cursorPosition").toInt())
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) return;
requestCompletions(editor); requestCompletions(editor);
}); });
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() { connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
delete m_scheduledRequests.take(editor); delete m_scheduledRequests.take(editor);
cancelRunningRequest(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); it = m_scheduledRequests.insert(editor, timer);
} }
@@ -307,11 +369,9 @@ void QodeAssistClient::handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor) const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
{ {
m_progressHandler.hideProgress(); m_progressHandler.hideProgress();
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode(); const auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 1) {
editor->abortAssist(); editor->abortAssist();
}
if (response.error()) { if (response.error()) {
log(*response.error()); log(*response.error());
@@ -325,12 +385,25 @@ void QodeAssistClient::handleCompletions(
requestPosition = requestParams->position().toPositionInDocument(editor->document()); requestPosition = requestParams->position().toPositionInDocument(editor->document());
const MultiTextCursor cursors = editor->multiTextCursor(); const MultiTextCursor cursors = editor->multiTextCursor();
if (cursors.hasMultipleCursors()) if (cursors.hasMultipleCursors() || cursors.hasSelection())
return; return;
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition) const int currentPosition = cursors.mainCursor().position();
if (requestPosition < 0 || currentPosition < requestPosition)
return; 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()) { if (const std::optional<GetCompletionResponse> result = response.result()) {
auto isValidCompletion = [](const Completion &completion) { auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty(); return completion.isValid() && !completion.text().trimmed().isEmpty();
@@ -338,34 +411,58 @@ void QodeAssistClient::handleCompletions(
QList<Completion> completions QList<Completion> completions
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion); = Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
QList<Completion> matchedCompletions;
matchedCompletions.reserve(completions.size());
for (Completion &completion : completions) { for (Completion &completion : completions) {
const LanguageServerProtocol::Range range = completion.range(); const LanguageServerProtocol::Range range = completion.range();
if (range.start().line() != range.end().line()) if (range.start().line() != range.end().line())
continue; continue;
const QString completionText = completion.text(); QString completionText = completion.text();
const int end = int(completionText.size()) - 1; const int end = int(completionText.size()) - 1;
int delta = 0; int delta = 0;
while (delta <= end && completionText[end - delta].isSpace()) while (delta <= end && completionText[end - delta].isSpace())
++delta; ++delta;
if (delta > 0) 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) { auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()}; 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::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
Text::Position pos{toTextPos(c.position())}; Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()}; 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())); editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
} }
} }
@@ -376,12 +473,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
if (it == m_runningRequests.constEnd()) if (it == m_runningRequests.constEnd())
return; return;
m_progressHandler.hideProgress(); 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()); cancelRequest(it->id());
m_runningRequests.erase(it); m_runningRequests.erase(it);
} }
@@ -423,17 +514,6 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear(); 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) void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{ {
m_progressHandler.hideProgress(); 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) void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
{ {
TextEditorWidget *editorWidget = result.editor; TextEditorWidget *editorWidget = result.editor;
@@ -604,58 +677,20 @@ void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor
editCursor.endEditBlock(); editCursor.endEditBlock();
} }
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
int charsAdded,
bool isSpaceOrTab)
{ {
Q_UNUSED(isSpaceOrTab); const QTextCursor cursor = widget->textCursor();
const auto &settings = Settings::codeCompletionSettings();
const bool smart = settings.smartContextTrigger();
if (m_recentCharCount if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) { return;
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
const int charThreshold = settings.autoCompletionCharThreshold();
if (eager || m_recentCharCount > charThreshold)
scheduleRequest(widget); 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) bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
@@ -667,46 +702,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
if (event->type() == QEvent::KeyPress) { if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event); 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 (keyEvent->key() == Qt::Key_Escape) {
if (m_runningRequests.contains(editor)) { if (m_runningRequests.contains(editor)) {
cancelRunningRequest(editor); cancelRunningRequest(editor);
@@ -724,8 +719,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
} }
m_progressHandler.hideProgress(); m_progressHandler.hideProgress();
m_hintHideTimer.stop();
m_hintHandler.hideHint();
} }
} }

View File

@@ -1,26 +1,6 @@
/* // Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2024-2026 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* 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/>.
*/
#pragma once #pragma once
@@ -32,12 +12,11 @@
#include "RefactorSuggestionHoverHandler.hpp" #include "RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp" #include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp" #include "widgets/CompletionErrorHandler.hpp"
#include "widgets/CompletionHintHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp" #include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp" #include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h> #include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp> #include <pluginllmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp> #include <pluginllmcore/IProviderRegistry.hpp>
namespace QodeAssist { namespace QodeAssist {
@@ -54,9 +33,6 @@ public:
void requestCompletions(TextEditor::TextEditorWidget *editor); void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor( void requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions = QString()); TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
bool isHintVisible() const;
void hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor);
protected: protected:
bool eventFilter(QObject *watched, QEvent *event) override; bool eventFilter(QObject *watched, QEvent *event) override;
@@ -75,8 +51,7 @@ private:
void displayRefactoringWidget(const RefactorResult &result); void displayRefactoringWidget(const RefactorResult &result);
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text); void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab); void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests; QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests; QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@@ -85,10 +60,8 @@ private:
QElapsedTimer m_typingTimer; QElapsedTimer m_typingTimer;
int m_recentCharCount; int m_recentCharCount;
QTimer m_hintHideTimer;
CompletionProgressHandler m_progressHandler; CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler; CompletionErrorHandler m_errorHandler;
CompletionHintHandler m_hintHandler;
EditorChatButtonHandler m_chatButtonHandler; EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr}; QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr}; RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};

View File

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

View File

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

View File

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

View File

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

166
README.md
View File

@@ -1,10 +1,12 @@
# QodeAssist - AI-powered coding assistant plugin for Qt Creator # QodeAssist AI coding assistant 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-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** ⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services: > When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
@@ -29,13 +31,15 @@
QodeAssist enhances Qt Creator with AI-powered coding assistance: QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML - **Code Completion** — intelligent, context-aware suggestions (FIM and chat models) for C++ and QML, with multiline support
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel) - **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library - **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
- **File Context**: Attach or link files for better AI understanding - **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
- **Tool Calling**: AI can read project files, search code, and access diagnostics - **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more - **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)
- **Customizable**: Project-specific rules, custom instructions, and extensive model templates - **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! **Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
@@ -125,21 +129,64 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
## Configuration ## Configuration
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide: ### Quick Setup (Recommended for Beginners)
### Supported Providers The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
<details>
<summary>Quick setup: (click to expand)</summary>
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
</details>
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider 1. **Open QodeAssist Settings**
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server 2. **Select a Preset** - Choose from the Quick Setup dropdown:
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider - **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider - **OpenAI** (gpt-5.2-codex)
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider - **Mistral AI** (Codestral 2501)
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider - **Google AI** (Gemini 2.5 Flash)
- **LM Studio** - Local LLM provider 3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
### Manual Provider Configuration
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
**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
For optimal coding assistance, we recommend using these top-tier models:
**Best for Code Completion & Refactoring:**
- **Claude 4.5 Haiku or Sonnet** (Anthropic)
- **GPT-5.1-codex or codex-mini** (OpenAI Responses API)
- **Codestral** (Mistral)
**Best for Chat Assistant:**
- **Claude 4.5 Sonnet** (Anthropic) - Outstanding reasoning and natural conversation flow
- **GPT-5.1-codex** (OpenAI Responses API) - Latest model with advanced capabilities
- **Gemini 2.5 or 3.0** (Google AI) - Latest models from Google
- **Mistral large** (Mistral) - Fast and capable
**Local models:**
- **Qwen3-coder** (Qwen) - Best local models
### Additional Configuration ### Additional Configuration
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project - **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore` - **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
@@ -176,6 +223,8 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- Multiple chat panels: side panel, bottom panel, and popup window - Multiple chat panels: side panel, bottom panel, and popup window
- Chat history with auto-save and restore - Chat history with auto-save and restore
- Token usage monitoring - Token usage monitoring
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
- **[File Context](docs/file-context.md)** - Attach or link files for better context - **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional) - Automatic syncing with open editor files (optional)
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks - Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
@@ -188,10 +237,37 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- **[Learn more](docs/quick-refactoring.md)** - **[Learn more](docs/quick-refactoring.md)**
### Tools & Function Calling ### Tools & Function Calling
- Read project files
- List and search in project Chat and Quick Refactor can call tools to inspect and modify your project. Each tool can be individually enabled/disabled in settings.
- Access linter/compiler issues
- Enabled by default (can be disabled) | 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 |
### 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 ## Context Layers
@@ -260,22 +336,24 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
│ CHAT ASSISTANT │ │ CHAT ASSISTANT │
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Chat Assistant Settings) │ │ 1. System Prompt (from Chat Assistant Settings) │
│ 2. Project Rules: │ 2. Agent Role (optional, from role selector):
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
│ 3. Project Rules: │
│ ├─ .qodeassist/rules/common/*.md │ │ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/chat/*.md │ │ └─ .qodeassist/rules/chat/*.md │
3. File Context (optional): │ 4. File Context (optional): │
│ ├─ Attached files (manual) │ │ ├─ Attached files (manual) │
│ ├─ Linked files (persistent) │ │ ├─ Linked files (persistent) │
│ └─ Open editor files (if auto-sync enabled) │ │ └─ Open editor files (if auto-sync enabled) │
4. Tool Definitions (if enabled): │ 5. Tool Definitions (if enabled): │
│ ├─ ReadProjectFileByName │ │ ├─ ReadProjectFileByName │
│ ├─ ListProjectFiles │ │ ├─ ListProjectFiles │
│ ├─ SearchInProject │ │ ├─ SearchInProject │
│ └─ GetIssuesList │ │ └─ GetIssuesList │
5. Conversation History │ 6. Conversation History │
6. User Message │ 7. User Message │
│ │ │ │
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
│ [History: Previous messages] │ │ [History: Previous messages] │
│ [User: FileContext + UserMessage] │ │ [User: FileContext + UserMessage] │
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
@@ -321,6 +399,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure - **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
- **System Prompts** are configured independently for each feature in Settings - **System Prompts** are configured independently for each feature in Settings
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
- **FIM vs Non-FIM models** for code completion use different System Prompts: - **FIM vs Non-FIM models** for code completion use different System Prompts:
- FIM models: Direct completion prompt - FIM models: Direct completion prompt
- Non-FIM models: Prompt includes response formatting instructions - Non-FIM models: Prompt includes response formatting instructions
@@ -328,14 +407,14 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
- **Custom Instructions** provide reusable templates that can be augmented with specific details - **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled - **Tool Calling** is available for Chat and Quick Refactor when enabled
See [Project Rules Documentation](docs/project-rules.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details. See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
## QtCreator Version Compatibility ## QtCreator Version Compatibility
| Qt Creator Version | QodeAssist Version | | Qt Creator Version | QodeAssist Version |
|-------------------|-------------------| |-------------------|-------------------|
| 17.0.0+ | 0.6.0 - 0.x.x | | 17.0.0+ | 0.6.0 - 0.x.x |
| 16.0.2 | 0.5.13 - 0.x.x | | 16.0.2 | 0.5.13 - 0.9.6 |
| 16.0.1 | 0.5.7 - 0.5.13 | | 16.0.1 | 0.5.7 - 0.5.13 |
| 16.0.0 | 0.5.2 - 0.5.6 | | 16.0.0 | 0.5.2 - 0.5.6 |
| 15.0.1 | 0.4.8 - 0.5.1 | | 15.0.1 | 0.4.8 - 0.5.1 |
@@ -370,14 +449,16 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
## Development Progress ## Development Progress
- [x] Code completion functionality - [x] Code completion (FIM and chat models)
- [x] Chat assistant with multiple panels - [x] Chat assistant (side / bottom / detached panels)
- [x] Quick refactoring with custom-instructions library
- [x] Diff sharing with models - [x] Diff sharing with models
- [x] Tools/function calling support - [x] Tools / function calling (file I/O, build, terminal, diagnostics)
- [x] Project-specific rules - [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 - [ ] Full project source sharing
- [ ] Additional provider support - [ ] Additional provider support
- [ ] MCP (Model Context Protocol) support
## Support the development of QodeAssist ## Support the development of QodeAssist
If you find QodeAssist helpful, there are several ways you can support the project: If you find QodeAssist helpful, there are several ways you can support the project:
@@ -396,6 +477,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! 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 ## How to Build
### Prerequisites ### Prerequisites

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2025-2026 Petr Mironychev
* Copyright (C) 2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* 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:
*/
#include "FlowEditor.hpp" #include "FlowEditor.hpp"

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2025-2026 Petr Mironychev
* Copyright (C) 2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* 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:
*/
#pragma once #pragma once

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,12 @@ qt_add_qml_module(QodeAssistUIControls
QML_FILES QML_FILES
qml/Badge.qml qml/Badge.qml
qml/QoAButton.qml qml/QoAButton.qml
qml/QoABusyOverlay.qml
qml/QoATextSlider.qml qml/QoATextSlider.qml
qml/QoAComboBox.qml qml/QoAComboBox.qml
qml/FadeListItemAnimation.qml qml/FadeListItemAnimation.qml
qml/QoASeparator.qml
qml/QoAToolTip.qml
RESOURCES RESOURCES
icons/dropdown-arrow-light.svg icons/dropdown-arrow-light.svg

View File

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

View File

@@ -0,0 +1,43 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
Rectangle {
id: root
property alias text: label.text
property bool active: false
visible: active
color: Qt.rgba(palette.window.r, palette.window.g, palette.window.b, 0.75)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
hoverEnabled: true
preventStealing: true
onWheel: function(wheel) { wheel.accepted = true }
}
Column {
anchors.centerIn: parent
spacing: 10
BusyIndicator {
anchors.horizontalCenter: parent.horizontalCenter
running: root.active
implicitWidth: 36
implicitHeight: 36
}
Text {
id: label
anchors.horizontalCenter: parent.horizontalCenter
color: palette.text
font.pixelSize: 13
}
}
}

View File

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

View File

@@ -1,21 +1,5 @@
/* // Copyright (C) 2025-2026 Petr Mironychev
* Copyright (C) 2025 Petr Mironychev // SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -24,9 +8,26 @@ import QtQuick.Controls.Basic as Basic
Basic.ComboBox { Basic.ComboBox {
id: control id: control
property real popupContentWidth: 100
implicitWidth: Math.min(contentItem.implicitWidth + 8, 300) implicitWidth: Math.min(contentItem.implicitWidth + 8, 300)
implicitHeight: 30 implicitHeight: 30
function updatePopupWidth() {
var maxWidth = 100;
if (model) {
for (var i = 0; i < model.length; i++) {
textMetrics.text = model[i];
maxWidth = Math.max(maxWidth, textMetrics.width + 40);
}
}
popupContentWidth = Math.min(maxWidth, 350);
}
onModelChanged: updatePopupWidth()
Component.onCompleted: updatePopupWidth()
clip: true
indicator: Image { indicator: Image {
id: dropdownIcon id: dropdownIcon
@@ -94,7 +95,7 @@ Basic.ComboBox {
popup: Popup { popup: Popup {
y: control.height + 2 y: control.height + 2
width: control.width width: Math.max(control.width, control.popupContentWidth)
implicitHeight: Math.min(contentItem.implicitHeight, 300) implicitHeight: Math.min(contentItem.implicitHeight, 300)
padding: 4 padding: 4
@@ -128,7 +129,7 @@ Basic.ComboBox {
} }
delegate: ItemDelegate { delegate: ItemDelegate {
width: control.width - 8 width: control.popup.width - 8
height: 32 height: 32
contentItem: Text { contentItem: Text {
@@ -157,5 +158,10 @@ Basic.ComboBox {
} }
} }
} }
TextMetrics {
id: textMetrics
font.pixelSize: 12
}
} }

View File

@@ -0,0 +1,12 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
Rectangle {
id: root
height: 15
width: 1
color: palette.mid
}

View File

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

View File

@@ -0,0 +1,68 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
ToolTip {
id: root
padding: 8
contentItem: Text {
text: root.text
font: root.font
color: palette.toolTipText
wrapMode: Text.Wrap
}
background: Item {
implicitWidth: bg.implicitWidth
implicitHeight: bg.implicitHeight
Rectangle {
anchors.fill: bg
anchors.margins: -2
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.12)
radius: 8
z: -2
}
Rectangle {
anchors.fill: bg
anchors.margins: -1
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.08)
radius: 7
z: -1
}
Rectangle {
id: bg
anchors.fill: parent
color: palette.toolTipBase
border.color: Qt.darker(palette.toolTipBase, 1.2)
border.width: 1
radius: 6
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0.0
to: 1.0
duration: 150
easing.type: Easing.OutQuad
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1.0
to: 0.0
duration: 100
easing.type: Easing.InQuad
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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