mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-13 09:49:12 -04:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f4bda51cd | ||
|
|
7483c78777 | ||
|
|
a3ad314cd4 | ||
|
|
74c899c8c3 | ||
|
|
6addcedfd0 | ||
|
|
eb7fc2f7b4 | ||
|
|
a06320d1c4 | ||
|
|
b1ca6823b8 | ||
|
|
cc2d42f6d7 | ||
|
|
4faeb90dc0 | ||
|
|
9f7497d15c | ||
|
|
cab2f0a55e | ||
|
|
7704bffd88 | ||
|
|
3b421f60af | ||
|
|
86f4635080 | ||
|
|
f21757b9b3 | ||
|
|
9bb6d55687 | ||
|
|
bbb9c47cbb | ||
|
|
46aa53e726 | ||
|
|
4d320bc065 | ||
|
|
7b4e08859c | ||
|
|
d15b46825e | ||
|
|
e0ab5080ea | ||
|
|
6a8fbe1792 | ||
|
|
d867a6f0be | ||
|
|
248530c746 | ||
|
|
c73b71f328 | ||
|
|
d2c1e39a2e | ||
|
|
e86e7e103e | ||
|
|
42199024ff | ||
|
|
620fded2e1 | ||
|
|
90b7ed26b1 | ||
|
|
25c4d5f185 | ||
|
|
7a551ed384 | ||
|
|
ca0a47b160 | ||
|
|
6b069b55e3 | ||
|
|
2891b313d2 | ||
|
|
ede2c01eb7 | ||
|
|
6c05f0d594 | ||
|
|
15d714588f | ||
|
|
9a2ba08538 | ||
|
|
37084bec59 | ||
|
|
6910037e97 | ||
|
|
a72cdd85a4 | ||
|
|
31b4e73af5 | ||
|
|
088887c802 | ||
|
|
b7a9787cc3 | ||
|
|
e2e13f0f38 | ||
|
|
49ae335d7d | ||
|
|
2ba58a403f | ||
|
|
3de1619bf0 | ||
|
|
ec45067336 | ||
|
|
52fb65c5b1 | ||
|
|
478f369ad2 | ||
|
|
762c965377 | ||
|
|
d2b93310e2 | ||
|
|
f3b1e7f411 | ||
|
|
a55c6ccfdb | ||
|
|
b32433c336 | ||
|
|
6f11260cd1 | ||
|
|
ddd6aba091 | ||
|
|
e3f464c54e | ||
|
|
e86e58337a | ||
|
|
dbd47387be | ||
|
|
50e1276ab2 | ||
|
|
50c948ccfe | ||
|
|
949dad4fd2 |
14
.github/workflows/build_cmake.yml
vendored
14
.github/workflows/build_cmake.yml
vendored
@@ -46,20 +46,18 @@ jobs:
|
||||
}
|
||||
qt_config:
|
||||
- {
|
||||
qt_version: "6.8.3",
|
||||
qt_creator_version: "16.0.2"
|
||||
qt_version: "6.10.1",
|
||||
qt_creator_version: "18.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.9.2",
|
||||
qt_creator_version: "17.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.10.0",
|
||||
qt_creator_version: "18.0.0"
|
||||
qt_version: "6.10.3",
|
||||
qt_creator_version: "19.0.2"
|
||||
}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Checkout submodules
|
||||
id: git
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -78,3 +78,5 @@ CMakeLists.txt.user*
|
||||
/.cursor
|
||||
/.vscode
|
||||
.qtc_clangd/compile_commands.json
|
||||
CLAUDE.md
|
||||
/.claude
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -0,0 +1,3 @@
|
||||
[submodule "sources/external/llmqore"]
|
||||
path = sources/external/llmqore
|
||||
url = https://github.com/Palm1r/llmqore.git
|
||||
|
||||
@@ -34,7 +34,9 @@ add_definitions(
|
||||
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
||||
)
|
||||
|
||||
add_subdirectory(llmcore)
|
||||
add_subdirectory(sources/external/llmqore)
|
||||
add_subdirectory(sources/skills)
|
||||
add_subdirectory(pluginllmcore)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(logger)
|
||||
add_subdirectory(UIControls)
|
||||
@@ -61,6 +63,9 @@ add_qtc_plugin(QodeAssist
|
||||
QtCreator::ExtensionSystem
|
||||
QtCreator::Utils
|
||||
QtCreator::CPlusPlus
|
||||
LLMQore
|
||||
PluginLLMCore
|
||||
Skills
|
||||
QodeAssistChatViewplugin
|
||||
SOURCES
|
||||
.github/workflows/build_cmake.yml
|
||||
@@ -78,8 +83,6 @@ add_qtc_plugin(QodeAssist
|
||||
templates/OpenAI.hpp
|
||||
templates/MistralAI.hpp
|
||||
templates/StarCoder2Fim.hpp
|
||||
# templates/DeepSeekCoderFim.hpp
|
||||
# templates/CustomFimTemplate.hpp
|
||||
templates/Qwen25CoderFIM.hpp
|
||||
templates/OpenAICompatible.hpp
|
||||
templates/Llama3.hpp
|
||||
@@ -92,27 +95,20 @@ add_qtc_plugin(QodeAssist
|
||||
templates/Qwen3CoderFIM.hpp
|
||||
templates/OpenAIResponses.hpp
|
||||
providers/Providers.hpp
|
||||
providers/ProviderUrlUtils.hpp
|
||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
||||
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
|
||||
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
|
||||
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
|
||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
||||
providers/OpenAIResponses/ModelRequest.hpp
|
||||
providers/OpenAIResponses/ResponseObject.hpp
|
||||
providers/OpenAIResponses/GetResponseRequest.hpp
|
||||
providers/OpenAIResponses/DeleteResponseRequest.hpp
|
||||
providers/OpenAIResponses/CancelResponseRequest.hpp
|
||||
providers/OpenAIResponses/ListInputItemsRequest.hpp
|
||||
providers/OpenAIResponses/InputTokensRequest.hpp
|
||||
providers/OpenAIResponses/ItemTypesReference.hpp
|
||||
providers/OpenAIResponsesRequestBuilder.hpp
|
||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
||||
providers/OpenAIResponsesMessage.hpp providers/OpenAIResponsesMessage.cpp
|
||||
QodeAssist.qrc
|
||||
LSPCompletion.hpp
|
||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||
@@ -121,6 +117,9 @@ add_qtc_plugin(QodeAssist
|
||||
QodeAssistClient.hpp QodeAssistClient.cpp
|
||||
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
||||
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
||||
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
||||
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||
CodeHandler.hpp CodeHandler.cpp
|
||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||
@@ -141,24 +140,24 @@ add_qtc_plugin(QodeAssist
|
||||
widgets/DiffStatistics.hpp
|
||||
|
||||
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
||||
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
||||
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
|
||||
tools/ToolHandler.hpp tools/ToolHandler.cpp
|
||||
tools/ToolsRegistration.hpp tools/ToolsRegistration.cpp
|
||||
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
|
||||
tools/ToolsManager.hpp tools/ToolsManager.cpp
|
||||
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
||||
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
||||
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
||||
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
|
||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
||||
tools/FindFileTool.hpp tools/FindFileTool.cpp
|
||||
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
|
||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||
tools/TodoTool.hpp tools/TodoTool.cpp
|
||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
||||
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
|
||||
tools/ReadOriginalHistoryTool.hpp tools/ReadOriginalHistoryTool.cpp
|
||||
tools/SkillTool.hpp tools/SkillTool.cpp
|
||||
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
|
||||
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
|
||||
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
|
||||
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||
)
|
||||
|
||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||
|
||||
123
ChatView/AgentRoleController.cpp
Normal file
123
ChatView/AgentRoleController.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "AgentRoleController.hpp"
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "AgentRole.hpp"
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
AgentRoleController::AgentRoleController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().systemPrompt,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&AgentRoleController::baseSystemPromptChanged);
|
||||
|
||||
loadAvailableRoles();
|
||||
}
|
||||
|
||||
QStringList AgentRoleController::availableRoles() const
|
||||
{
|
||||
return m_availableRoles;
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRole() const
|
||||
{
|
||||
return m_currentRole;
|
||||
}
|
||||
|
||||
QString AgentRoleController::baseSystemPrompt() const
|
||||
{
|
||||
return Settings::chatAssistantSettings().systemPrompt();
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRoleDescription() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return Settings::AgentRolesManager::getNoRole().description;
|
||||
|
||||
return role.description;
|
||||
}
|
||||
|
||||
QString AgentRoleController::currentRoleSystemPrompt() const
|
||||
{
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
if (lastRoleId.isEmpty())
|
||||
return QString();
|
||||
|
||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||
if (role.id.isEmpty())
|
||||
return QString();
|
||||
|
||||
return role.systemPrompt;
|
||||
}
|
||||
|
||||
void AgentRoleController::loadAvailableRoles()
|
||||
{
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
m_availableRoles.clear();
|
||||
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
||||
|
||||
for (const auto &role : roles)
|
||||
m_availableRoles.append(role.name);
|
||||
|
||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
||||
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
|
||||
|
||||
if (!lastRoleId.isEmpty()) {
|
||||
for (const auto &role : roles) {
|
||||
if (role.id == lastRoleId) {
|
||||
m_currentRole = role.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit availableRolesChanged();
|
||||
emit currentRoleChanged();
|
||||
}
|
||||
|
||||
void AgentRoleController::applyRole(const QString &roleName)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
||||
settings.lastUsedRoleId.setValue("");
|
||||
settings.writeSettings();
|
||||
m_currentRole = roleName;
|
||||
emit currentRoleChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
||||
|
||||
for (const auto &role : roles) {
|
||||
if (role.name == roleName) {
|
||||
settings.lastUsedRoleId.setValue(role.id);
|
||||
settings.writeSettings();
|
||||
m_currentRole = role.name;
|
||||
emit currentRoleChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgentRoleController::openSettings()
|
||||
{
|
||||
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
38
ChatView/AgentRoleController.hpp
Normal file
38
ChatView/AgentRoleController.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class AgentRoleController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRoleController(QObject *parent = nullptr);
|
||||
|
||||
QStringList availableRoles() const;
|
||||
QString currentRole() const;
|
||||
QString baseSystemPrompt() const;
|
||||
QString currentRoleDescription() const;
|
||||
QString currentRoleSystemPrompt() const;
|
||||
|
||||
void loadAvailableRoles();
|
||||
void applyRole(const QString &roleName);
|
||||
void openSettings();
|
||||
|
||||
signals:
|
||||
void availableRolesChanged();
|
||||
void currentRoleChanged();
|
||||
void baseSystemPromptChanged();
|
||||
|
||||
private:
|
||||
QStringList m_availableRoles;
|
||||
QString m_currentRole;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -20,8 +20,10 @@ qt_add_qml_module(QodeAssistChatView
|
||||
|
||||
qml/controls/AttachedFilesPlace.qml
|
||||
qml/controls/BottomBar.qml
|
||||
qml/controls/FileMentionPopup.qml
|
||||
qml/controls/FileEditsActionBar.qml
|
||||
qml/controls/ContextViewer.qml
|
||||
qml/controls/SkillCommandPopup.qml
|
||||
qml/controls/Toast.qml
|
||||
qml/controls/TopBar.qml
|
||||
qml/controls/SplitDropZone.qml
|
||||
@@ -68,6 +70,13 @@ qt_add_qml_module(QodeAssistChatView
|
||||
FileItem.hpp FileItem.cpp
|
||||
ChatFileManager.hpp ChatFileManager.cpp
|
||||
ChatCompressor.hpp ChatCompressor.cpp
|
||||
AgentRoleController.hpp AgentRoleController.cpp
|
||||
ChatConfigurationController.hpp ChatConfigurationController.cpp
|
||||
FileEditController.hpp FileEditController.cpp
|
||||
InputTokenCounter.hpp InputTokenCounter.cpp
|
||||
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
||||
FileMentionItem.hpp FileMentionItem.cpp
|
||||
SessionFileRegistry.hpp SessionFileRegistry.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
@@ -78,13 +87,15 @@ target_link_libraries(QodeAssistChatView
|
||||
Qt::Network
|
||||
QtCreator::Core
|
||||
QtCreator::Utils
|
||||
LLMCore
|
||||
PluginLLMCore
|
||||
QodeAssistSettings
|
||||
Context
|
||||
QodeAssistUIControlsplugin
|
||||
QodeAssistLogger
|
||||
LLMQore
|
||||
Skills
|
||||
)
|
||||
|
||||
target_include_directories(QodeAssistChatView
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatCompressor.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "PromptTemplateManager.hpp"
|
||||
@@ -56,7 +42,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
}
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
m_provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!m_provider) {
|
||||
emit compressionFailed(tr("No provider available"));
|
||||
@@ -64,7 +50,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
}
|
||||
|
||||
auto templateName = Settings::generalSettings().caTemplate();
|
||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||
templateName);
|
||||
|
||||
if (!promptTemplate) {
|
||||
@@ -76,30 +62,22 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
||||
m_chatModel = chatModel;
|
||||
m_originalChatPath = chatFilePath;
|
||||
m_accumulatedSummary.clear();
|
||||
m_currentRequestId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
emit compressionStarted();
|
||||
|
||||
connectProviderSignals();
|
||||
|
||||
QUrl requestUrl;
|
||||
QJsonObject payload;
|
||||
|
||||
if (m_provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
requestUrl = QUrl(QString("%1/models/%2:streamGenerateContent?alt=sse")
|
||||
.arg(Settings::generalSettings().caUrl(),
|
||||
Settings::generalSettings().caModel()));
|
||||
} else {
|
||||
requestUrl = QUrl(QString("%1%2").arg(Settings::generalSettings().caUrl(),
|
||||
m_provider->chatEndpoint()));
|
||||
payload["model"] = Settings::generalSettings().caModel();
|
||||
payload["stream"] = true;
|
||||
}
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
|
||||
buildRequestPayload(payload, promptTemplate);
|
||||
|
||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
m_currentRequestId = m_provider->sendRequest(
|
||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||
m_provider->sendRequest(m_currentRequestId, requestUrl, payload);
|
||||
}
|
||||
|
||||
bool ChatCompressor::isCompressing() const
|
||||
@@ -188,28 +166,28 @@ QString ChatCompressor::buildCompressionPrompt() const
|
||||
}
|
||||
|
||||
void ChatCompressor::buildRequestPayload(
|
||||
QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate)
|
||||
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
||||
{
|
||||
LLMCore::ContextData context;
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
context.systemPrompt = QStringLiteral(
|
||||
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||
|
||||
QVector<LLMCore::Message> messages;
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool
|
||||
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||
continue;
|
||||
|
||||
LLMCore::Message apiMessage;
|
||||
PluginLLMCore::Message apiMessage;
|
||||
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
LLMCore::Message compressionRequest;
|
||||
PluginLLMCore::Message compressionRequest;
|
||||
compressionRequest.role = "user";
|
||||
compressionRequest.content = buildCompressionPrompt();
|
||||
messages.append(compressionRequest);
|
||||
@@ -217,7 +195,7 @@ void ChatCompressor::buildRequestPayload(
|
||||
context.history = messages;
|
||||
|
||||
m_provider->prepareRequest(
|
||||
payload, promptTemplate, context, LLMCore::RequestType::Chat, false, false);
|
||||
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
||||
}
|
||||
|
||||
bool ChatCompressor::createCompressedChatFile(
|
||||
@@ -250,6 +228,8 @@ bool ChatCompressor::createCompressedChatFile(
|
||||
summaryMessage["images"] = QJsonArray();
|
||||
|
||||
root["messages"] = QJsonArray{summaryMessage};
|
||||
root["compressedFrom"] = sourcePath;
|
||||
root["compressedAt"] = QDateTime::currentDateTime().toString(Qt::ISODate);
|
||||
|
||||
if (QFile::exists(destPath))
|
||||
QFile::remove(destPath);
|
||||
@@ -266,23 +246,25 @@ bool ChatCompressor::createCompressedChatFile(
|
||||
|
||||
void ChatCompressor::connectProviderSignals()
|
||||
{
|
||||
auto *c = m_provider->client();
|
||||
|
||||
m_connections.append(connect(
|
||||
m_provider,
|
||||
&LLMCore::Provider::partialResponseReceived,
|
||||
c,
|
||||
&::LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ChatCompressor::onPartialResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
m_provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&ChatCompressor::onFullResponseReceived,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
m_connections.append(connect(
|
||||
m_provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
c,
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ChatCompressor::onRequestFailed,
|
||||
Qt::UniqueConnection));
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -24,10 +8,10 @@
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
namespace QodeAssist::PluginLLMCore {
|
||||
class Provider;
|
||||
class PromptTemplate;
|
||||
} // namespace QodeAssist::LLMCore
|
||||
} // namespace QodeAssist::PluginLLMCore
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@@ -64,13 +48,13 @@ private:
|
||||
void disconnectAllSignals();
|
||||
void cleanupState();
|
||||
void handleCompressionError(const QString &error);
|
||||
void buildRequestPayload(QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate);
|
||||
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
||||
|
||||
bool m_isCompressing = false;
|
||||
QString m_currentRequestId;
|
||||
QString m_originalChatPath;
|
||||
QString m_accumulatedSummary;
|
||||
LLMCore::Provider *m_provider = nullptr;
|
||||
PluginLLMCore::Provider *m_provider = nullptr;
|
||||
ChatModel *m_chatModel = nullptr;
|
||||
|
||||
QList<QMetaObject::Connection> m_connections;
|
||||
|
||||
99
ChatView/ChatConfigurationController.cpp
Normal file
99
ChatView/ChatConfigurationController.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatConfigurationController.hpp"
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatConfigurationController::ChatConfigurationController(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
connect(
|
||||
&settings.caProvider,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatConfigurationController::updateCurrentConfiguration);
|
||||
connect(
|
||||
&settings.caModel,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatConfigurationController::updateCurrentConfiguration);
|
||||
|
||||
loadAvailableConfigurations();
|
||||
}
|
||||
|
||||
QStringList ChatConfigurationController::availableConfigurations() const
|
||||
{
|
||||
return m_availableConfigurations;
|
||||
}
|
||||
|
||||
QString ChatConfigurationController::currentConfiguration() const
|
||||
{
|
||||
return m_currentConfiguration;
|
||||
}
|
||||
|
||||
void ChatConfigurationController::updateCurrentConfiguration()
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
m_currentConfiguration
|
||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
||||
emit currentConfigurationChanged();
|
||||
}
|
||||
|
||||
void ChatConfigurationController::loadAvailableConfigurations()
|
||||
{
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
manager.loadConfigurations(Settings::ConfigurationType::Chat);
|
||||
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::Chat);
|
||||
|
||||
m_availableConfigurations.clear();
|
||||
m_availableConfigurations.append(QObject::tr("Current Settings"));
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
m_availableConfigurations.append(config.name);
|
||||
}
|
||||
|
||||
updateCurrentConfiguration();
|
||||
|
||||
emit availableConfigurationsChanged();
|
||||
}
|
||||
|
||||
void ChatConfigurationController::applyConfiguration(const QString &configName)
|
||||
{
|
||||
if (configName == QObject::tr("Current Settings")) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::Chat);
|
||||
|
||||
for (const Settings::AIConfiguration &config : configs) {
|
||||
if (config.name == configName) {
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
settings.caProvider.setValue(config.provider);
|
||||
settings.caModel.setValue(config.model);
|
||||
settings.caTemplate.setValue(config.templateName);
|
||||
settings.caUrl.setValue(config.url);
|
||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
||||
|
||||
settings.writeSettings();
|
||||
|
||||
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
|
||||
emit currentConfigurationChanged();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
35
ChatView/ChatConfigurationController.hpp
Normal file
35
ChatView/ChatConfigurationController.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatConfigurationController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatConfigurationController(QObject *parent = nullptr);
|
||||
|
||||
QStringList availableConfigurations() const;
|
||||
QString currentConfiguration() const;
|
||||
|
||||
void loadAvailableConfigurations();
|
||||
void applyConfiguration(const QString &configName);
|
||||
|
||||
signals:
|
||||
void availableConfigurationsChanged();
|
||||
void currentConfigurationChanged();
|
||||
|
||||
private:
|
||||
void updateCurrentConfiguration();
|
||||
|
||||
QStringList m_availableConfigurations;
|
||||
QString m_currentConfiguration;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "Logger.hpp"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
228
ChatView/ChatHistoryStore.cpp
Normal file
228
ChatView/ChatHistoryStore.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatHistoryStore.hpp"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
{}
|
||||
|
||||
QString ChatHistoryStore::historyDir() const
|
||||
{
|
||||
QString path;
|
||||
|
||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||
} else {
|
||||
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||
path = baseDir.filePath("qodeassist/chat_history");
|
||||
}
|
||||
|
||||
QDir dir(path);
|
||||
if (!dir.exists() && !dir.mkpath(".")) {
|
||||
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
||||
return QString();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
QString ChatHistoryStore::suggestedFileName() const
|
||||
{
|
||||
QString shortMessage;
|
||||
|
||||
if (m_chatModel->rowCount() > 0) {
|
||||
QString firstMessage
|
||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
||||
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||
|
||||
if (shortMessage.isEmpty()) {
|
||||
QVariantList images
|
||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
||||
if (!images.isEmpty()) {
|
||||
shortMessage = "image_chat";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return generateChatFileName(shortMessage, historyDir());
|
||||
}
|
||||
|
||||
QString ChatHistoryStore::autosaveFilePath(const QString &recentFilePath) const
|
||||
{
|
||||
if (!recentFilePath.isEmpty()) {
|
||||
return recentFilePath;
|
||||
}
|
||||
|
||||
QString dir = historyDir();
|
||||
if (dir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QDir(dir).filePath(suggestedFileName() + ".json");
|
||||
}
|
||||
|
||||
QString ChatHistoryStore::autosaveFilePath(
|
||||
const QString &recentFilePath, const QString &firstMessage, bool hasImageAttachments) const
|
||||
{
|
||||
if (!recentFilePath.isEmpty()) {
|
||||
return recentFilePath;
|
||||
}
|
||||
|
||||
QString dir = historyDir();
|
||||
if (dir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||
|
||||
if (shortMessage.isEmpty() && hasImageAttachments) {
|
||||
shortMessage = "image_chat";
|
||||
}
|
||||
|
||||
QString fileName = generateChatFileName(shortMessage, dir);
|
||||
return QDir(dir).filePath(fileName + ".json");
|
||||
}
|
||||
|
||||
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
||||
{
|
||||
return ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||
}
|
||||
|
||||
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
||||
{
|
||||
return ChatSerializer::loadFromFile(m_chatModel, filePath);
|
||||
}
|
||||
|
||||
void ChatHistoryStore::showSaveDialog()
|
||||
{
|
||||
QString initialDir = historyDir();
|
||||
|
||||
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
|
||||
dialog->setAcceptMode(QFileDialog::AcceptSave);
|
||||
dialog->setFileMode(QFileDialog::AnyFile);
|
||||
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||
dialog->setDefaultSuffix("json");
|
||||
if (!initialDir.isEmpty()) {
|
||||
dialog->setDirectory(initialDir);
|
||||
dialog->selectFile(suggestedFileName() + ".json");
|
||||
}
|
||||
|
||||
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||
if (result == QFileDialog::Accepted) {
|
||||
QStringList files = dialog->selectedFiles();
|
||||
if (!files.isEmpty()) {
|
||||
emit saveRequested(files.first());
|
||||
}
|
||||
}
|
||||
dialog->deleteLater();
|
||||
});
|
||||
|
||||
dialog->open();
|
||||
}
|
||||
|
||||
void ChatHistoryStore::showLoadDialog()
|
||||
{
|
||||
QString initialDir = historyDir();
|
||||
|
||||
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
|
||||
dialog->setAcceptMode(QFileDialog::AcceptOpen);
|
||||
dialog->setFileMode(QFileDialog::ExistingFile);
|
||||
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||
if (!initialDir.isEmpty()) {
|
||||
dialog->setDirectory(initialDir);
|
||||
}
|
||||
|
||||
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||
if (result == QFileDialog::Accepted) {
|
||||
QStringList files = dialog->selectedFiles();
|
||||
if (!files.isEmpty()) {
|
||||
emit loadRequested(files.first());
|
||||
}
|
||||
}
|
||||
dialog->deleteLater();
|
||||
});
|
||||
|
||||
dialog->open();
|
||||
}
|
||||
|
||||
void ChatHistoryStore::openHistoryFolder() const
|
||||
{
|
||||
QString path;
|
||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||
} else {
|
||||
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||
path = baseDir.filePath("qodeassist/chat_history");
|
||||
}
|
||||
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
QString ChatHistoryStore::generateChatFileName(const QString &shortMessage, const QString &dir) const
|
||||
{
|
||||
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
|
||||
static const QRegularExpression underSymbols = QRegularExpression("_+");
|
||||
|
||||
QStringList parts;
|
||||
QString sanitizedMessage = shortMessage;
|
||||
sanitizedMessage.replace(saitizeSymbols, "_");
|
||||
sanitizedMessage.replace(underSymbols, "_");
|
||||
sanitizedMessage = sanitizedMessage.trimmed();
|
||||
|
||||
if (!sanitizedMessage.isEmpty()) {
|
||||
if (sanitizedMessage.startsWith('_')) {
|
||||
sanitizedMessage.remove(0, 1);
|
||||
}
|
||||
if (sanitizedMessage.endsWith('_')) {
|
||||
sanitizedMessage.chop(1);
|
||||
}
|
||||
|
||||
QString fullPath = QDir(dir).filePath(sanitizedMessage);
|
||||
QFileInfo fileInfo(fullPath);
|
||||
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
|
||||
parts << sanitizedMessage;
|
||||
}
|
||||
}
|
||||
|
||||
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
|
||||
|
||||
QString fileName = parts.join("_");
|
||||
QString fullPath = QDir(dir).filePath(fileName);
|
||||
QFileInfo finalCheck(fullPath);
|
||||
|
||||
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
||||
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
47
ChatView/ChatHistoryStore.hpp
Normal file
47
ChatView/ChatHistoryStore.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include "ChatSerializer.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel;
|
||||
|
||||
class ChatHistoryStore : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
|
||||
QString historyDir() const;
|
||||
QString suggestedFileName() const;
|
||||
QString autosaveFilePath(const QString &recentFilePath) const;
|
||||
QString autosaveFilePath(
|
||||
const QString &recentFilePath,
|
||||
const QString &firstMessage,
|
||||
bool hasImageAttachments) const;
|
||||
|
||||
SerializationResult save(const QString &filePath) const;
|
||||
SerializationResult load(const QString &filePath) const;
|
||||
|
||||
void showSaveDialog();
|
||||
void showLoadDialog();
|
||||
void openHistoryFolder() const;
|
||||
|
||||
signals:
|
||||
void saveRequested(const QString &filePath);
|
||||
void loadRequested(const QString &filePath);
|
||||
|
||||
private:
|
||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include <utils/aspects.h>
|
||||
@@ -27,7 +11,6 @@
|
||||
#include <QUrl>
|
||||
#include <QtQml>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "context/ChangesManager.h"
|
||||
|
||||
@@ -36,14 +19,6 @@ namespace QodeAssist::Chat {
|
||||
ChatModel::ChatModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
connect(
|
||||
&settings.chatTokensThreshold,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&ChatModel::tokensThresholdChanged);
|
||||
|
||||
connect(&Context::ChangesManager::instance(),
|
||||
&Context::ChangesManager::fileEditApplied,
|
||||
this,
|
||||
@@ -102,6 +77,16 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
case Roles::IsRedacted: {
|
||||
return message.isRedacted;
|
||||
}
|
||||
case Roles::PromptTokens:
|
||||
return message.promptTokens;
|
||||
case Roles::CompletionTokens:
|
||||
return message.completionTokens;
|
||||
case Roles::CachedPromptTokens:
|
||||
return message.cachedPromptTokens;
|
||||
case Roles::ReasoningTokens:
|
||||
return message.reasoningTokens;
|
||||
case Roles::TotalTokens:
|
||||
return message.promptTokens + message.completionTokens;
|
||||
case Roles::Images: {
|
||||
QVariantList imagesList;
|
||||
for (const auto &image : message.images) {
|
||||
@@ -117,8 +102,10 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||
QString fullPath = QDir(contentFolder).filePath(image.storedPath);
|
||||
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
||||
imageMap["filePath"] = fullPath;
|
||||
} else {
|
||||
imageMap["imageUrl"] = QString();
|
||||
imageMap["filePath"] = QString();
|
||||
}
|
||||
|
||||
imagesList.append(imageMap);
|
||||
@@ -138,6 +125,11 @@ QHash<int, QByteArray> ChatModel::roleNames() const
|
||||
roles[Roles::Attachments] = "attachments";
|
||||
roles[Roles::IsRedacted] = "isRedacted";
|
||||
roles[Roles::Images] = "images";
|
||||
roles[Roles::PromptTokens] = "promptTokens";
|
||||
roles[Roles::CompletionTokens] = "completionTokens";
|
||||
roles[Roles::CachedPromptTokens] = "cachedPromptTokens";
|
||||
roles[Roles::ReasoningTokens] = "reasoningTokens";
|
||||
roles[Roles::TotalTokens] = "totalTokens";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -221,6 +213,7 @@ void ChatModel::clear()
|
||||
m_messages.clear();
|
||||
endResetModel();
|
||||
emit modelReseted();
|
||||
emit sessionUsageChanged();
|
||||
}
|
||||
|
||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||
@@ -324,12 +317,6 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
|
||||
return messages;
|
||||
}
|
||||
|
||||
int ChatModel::tokensThreshold() const
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
return settings.chatTokensThreshold();
|
||||
}
|
||||
|
||||
QString ChatModel::lastMessageId() const
|
||||
{
|
||||
return !m_messages.isEmpty() ? m_messages.last().id : "";
|
||||
@@ -344,11 +331,15 @@ void ChatModel::resetModelTo(int index)
|
||||
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
|
||||
m_messages.remove(index, m_messages.size() - index);
|
||||
endRemoveRows();
|
||||
emit sessionUsageChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::addToolExecutionStatus(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &toolArguments)
|
||||
{
|
||||
QString content = toolName;
|
||||
|
||||
@@ -359,11 +350,15 @@ void ChatModel::addToolExecutionStatus(
|
||||
&& m_messages.last().role == ChatRole::Tool) {
|
||||
Message &lastMessage = m_messages.last();
|
||||
lastMessage.content = content;
|
||||
lastMessage.toolName = toolName;
|
||||
lastMessage.toolArguments = toolArguments;
|
||||
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
|
||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||
} else {
|
||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||
Message newMessage{ChatRole::Tool, content, toolId};
|
||||
newMessage.toolName = toolName;
|
||||
newMessage.toolArguments = toolArguments;
|
||||
m_messages.append(newMessage);
|
||||
endInsertRows();
|
||||
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
|
||||
@@ -372,6 +367,38 @@ void ChatModel::addToolExecutionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::dropTrailingAssistantMessage(const QString &requestId)
|
||||
{
|
||||
if (m_messages.isEmpty())
|
||||
return;
|
||||
|
||||
const Message &last = m_messages.last();
|
||||
if (last.role != ChatRole::Assistant || last.id != requestId)
|
||||
return;
|
||||
|
||||
const int idx = m_messages.size() - 1;
|
||||
beginRemoveRows(QModelIndex(), idx, idx);
|
||||
m_messages.removeLast();
|
||||
endRemoveRows();
|
||||
LOG_MESSAGE(QString("Dropped leaked pre-tool assistant message at index %1").arg(idx));
|
||||
}
|
||||
|
||||
void ChatModel::setToolMessageData(
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &toolArguments,
|
||||
const QString &toolResult)
|
||||
{
|
||||
for (int i = 0; i < m_messages.size(); ++i) {
|
||||
if (m_messages[i].role == ChatRole::Tool && m_messages[i].id == toolId) {
|
||||
m_messages[i].toolName = toolName;
|
||||
m_messages[i].toolArguments = toolArguments;
|
||||
m_messages[i].toolResult = toolResult;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::updateToolResult(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
|
||||
{
|
||||
@@ -391,6 +418,8 @@ void ChatModel::updateToolResult(
|
||||
for (int i = m_messages.size() - 1; i >= 0; --i) {
|
||||
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
|
||||
m_messages[i].content = toolName + "\n" + result;
|
||||
m_messages[i].toolName = toolName;
|
||||
m_messages[i].toolResult = result;
|
||||
emit dataChanged(index(i), index(i));
|
||||
toolMessageFound = true;
|
||||
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
|
||||
@@ -521,6 +550,62 @@ void ChatModel::updateMessageContent(const QString &messageId, const QString &ne
|
||||
}
|
||||
}
|
||||
|
||||
void ChatModel::setMessageUsage(
|
||||
const QString &messageId,
|
||||
int promptTokens,
|
||||
int completionTokens,
|
||||
int cachedPromptTokens,
|
||||
int reasoningTokens)
|
||||
{
|
||||
for (int i = 0; i < m_messages.size(); ++i) {
|
||||
if (m_messages[i].id != messageId)
|
||||
continue;
|
||||
m_messages[i].promptTokens = promptTokens;
|
||||
m_messages[i].completionTokens = completionTokens;
|
||||
m_messages[i].cachedPromptTokens = cachedPromptTokens;
|
||||
m_messages[i].reasoningTokens = reasoningTokens;
|
||||
emit dataChanged(
|
||||
index(i),
|
||||
index(i),
|
||||
{Roles::PromptTokens,
|
||||
Roles::CompletionTokens,
|
||||
Roles::CachedPromptTokens,
|
||||
Roles::ReasoningTokens,
|
||||
Roles::TotalTokens});
|
||||
emit sessionUsageChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int ChatModel::sessionPromptTokens() const
|
||||
{
|
||||
int total = 0;
|
||||
for (const auto &m : m_messages)
|
||||
total += m.promptTokens;
|
||||
return total;
|
||||
}
|
||||
|
||||
int ChatModel::sessionCompletionTokens() const
|
||||
{
|
||||
int total = 0;
|
||||
for (const auto &m : m_messages)
|
||||
total += m.completionTokens;
|
||||
return total;
|
||||
}
|
||||
|
||||
int ChatModel::sessionCachedPromptTokens() const
|
||||
{
|
||||
int total = 0;
|
||||
for (const auto &m : m_messages)
|
||||
total += m.cachedPromptTokens;
|
||||
return total;
|
||||
}
|
||||
|
||||
int ChatModel::sessionTotalTokens() const
|
||||
{
|
||||
return sessionPromptTokens() + sessionCompletionTokens();
|
||||
}
|
||||
|
||||
void ChatModel::setLoadingFromHistory(bool loading)
|
||||
{
|
||||
m_loadingFromHistory = loading;
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -24,6 +8,7 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
#include "context/ContentFile.hpp"
|
||||
@@ -33,14 +18,28 @@ namespace QodeAssist::Chat {
|
||||
class ChatModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
|
||||
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
|
||||
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
|
||||
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||
Q_ENUM(ChatRole)
|
||||
|
||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted, Images };
|
||||
enum Roles {
|
||||
RoleType = Qt::UserRole,
|
||||
Content,
|
||||
Attachments,
|
||||
IsRedacted,
|
||||
Images,
|
||||
PromptTokens,
|
||||
CompletionTokens,
|
||||
CachedPromptTokens,
|
||||
ReasoningTokens,
|
||||
TotalTokens
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
struct ImageAttachment
|
||||
@@ -60,6 +59,15 @@ public:
|
||||
|
||||
QList<Context::ContentFile> attachments;
|
||||
QList<ImageAttachment> images;
|
||||
|
||||
QString toolName;
|
||||
QJsonObject toolArguments;
|
||||
QString toolResult;
|
||||
|
||||
int promptTokens = 0;
|
||||
int completionTokens = 0;
|
||||
int cachedPromptTokens = 0;
|
||||
int reasoningTokens = 0;
|
||||
};
|
||||
|
||||
explicit ChatModel(QObject *parent = nullptr);
|
||||
@@ -82,15 +90,22 @@ public:
|
||||
QVector<Message> getChatHistory() const;
|
||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
||||
|
||||
int tokensThreshold() const;
|
||||
|
||||
QString currentModel() const;
|
||||
QString lastMessageId() const;
|
||||
|
||||
Q_INVOKABLE void resetModelTo(int index);
|
||||
|
||||
void addToolExecutionStatus(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &toolArguments);
|
||||
void dropTrailingAssistantMessage(const QString &requestId);
|
||||
void setToolMessageData(
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &toolArguments,
|
||||
const QString &toolResult);
|
||||
void updateToolResult(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
@@ -101,6 +116,18 @@ public:
|
||||
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
||||
|
||||
void setMessageUsage(
|
||||
const QString &messageId,
|
||||
int promptTokens,
|
||||
int completionTokens,
|
||||
int cachedPromptTokens,
|
||||
int reasoningTokens);
|
||||
|
||||
int sessionPromptTokens() const;
|
||||
int sessionCompletionTokens() const;
|
||||
int sessionCachedPromptTokens() const;
|
||||
int sessionTotalTokens() const;
|
||||
|
||||
void setLoadingFromHistory(bool loading);
|
||||
bool isLoadingFromHistory() const;
|
||||
|
||||
@@ -108,8 +135,8 @@ public:
|
||||
QString chatFilePath() const;
|
||||
|
||||
signals:
|
||||
void tokensThresholdChanged();
|
||||
void modelReseted();
|
||||
void sessionUsageChanged();
|
||||
|
||||
private slots:
|
||||
void onFileEditApplied(const QString &editId);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QPointer>
|
||||
#include <QQuickItem>
|
||||
#include <QVariantList>
|
||||
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "ClientInterface.hpp"
|
||||
#include "llmcore/PromptProviderChat.hpp"
|
||||
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatCompressor;
|
||||
class AgentRoleController;
|
||||
class ChatConfigurationController;
|
||||
class FileEditController;
|
||||
class InputTokenCounter;
|
||||
class ChatHistoryStore;
|
||||
class SessionFileRegistry;
|
||||
|
||||
class ChatRootView : public QQuickItem
|
||||
{
|
||||
@@ -72,6 +68,7 @@ class ChatRootView : public QQuickItem
|
||||
|
||||
public:
|
||||
ChatRootView(QQuickItem *parent = nullptr);
|
||||
~ChatRootView() override;
|
||||
|
||||
ChatModel *chatModel() const;
|
||||
QString currentTemplate() const;
|
||||
@@ -104,6 +101,13 @@ public:
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
Q_INVOKABLE void openSettings();
|
||||
|
||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||
|
||||
Q_INVOKABLE void relocateToSplit();
|
||||
Q_INVOKABLE void relocateToWindow();
|
||||
|
||||
void consumePendingChatFile();
|
||||
|
||||
Q_INVOKABLE void updateInputTokensCount();
|
||||
int inputTokensCount() const;
|
||||
|
||||
@@ -135,6 +139,8 @@ public:
|
||||
Q_INVOKABLE QString getRuleContent(int index);
|
||||
Q_INVOKABLE void refreshRules();
|
||||
|
||||
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
||||
|
||||
bool useTools() const;
|
||||
void setUseTools(bool enabled);
|
||||
bool useThinking() const;
|
||||
@@ -222,43 +228,66 @@ signals:
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
void compressionFailed(const QString &error);
|
||||
|
||||
void openFilesChanged();
|
||||
|
||||
void closeHostRequested();
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
QString getChatsHistoryDir() const;
|
||||
QString getSuggestedFileName() const;
|
||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||
void triggerOpenChatCommand(Utils::Id commandId);
|
||||
void handOffSession();
|
||||
bool deferSendForAutoCompress(
|
||||
const QString &message,
|
||||
const QStringList &attachments,
|
||||
const QStringList &linkedFiles,
|
||||
bool useTools,
|
||||
bool useThinking);
|
||||
void dispatchSend(
|
||||
const QString &message,
|
||||
const QStringList &attachments,
|
||||
const QStringList &linkedFiles,
|
||||
bool useTools,
|
||||
bool useThinking);
|
||||
bool hasImageAttachments(const QStringList &attachments) const;
|
||||
|
||||
SessionFileRegistry *sessionFileRegistry() const;
|
||||
Skills::SkillsManager *skillsManager() const;
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
LLMCore::PromptProviderChat m_promptProvider;
|
||||
PluginLLMCore::PromptProviderChat m_promptProvider;
|
||||
ClientInterface *m_clientInterface;
|
||||
ChatFileManager *m_fileManager;
|
||||
QString m_currentTemplate;
|
||||
QString m_recentFilePath;
|
||||
QStringList m_attachmentFiles;
|
||||
QStringList m_linkedFiles;
|
||||
int m_messageTokensCount{0};
|
||||
int m_inputTokensCount{0};
|
||||
|
||||
struct PendingSend {
|
||||
QString message;
|
||||
QStringList attachments;
|
||||
QStringList linkedFiles;
|
||||
bool useTools = false;
|
||||
bool useThinking = false;
|
||||
bool active = false;
|
||||
};
|
||||
PendingSend m_pendingSend;
|
||||
bool m_isSyncOpenFiles;
|
||||
QList<Core::IEditor *> m_currentEditors;
|
||||
bool m_isRequestInProgress;
|
||||
QString m_lastErrorMessage;
|
||||
QVariantList m_activeRules;
|
||||
|
||||
QString m_currentMessageRequestId;
|
||||
int m_currentMessageTotalEdits{0};
|
||||
int m_currentMessageAppliedEdits{0};
|
||||
int m_currentMessagePendingEdits{0};
|
||||
int m_currentMessageRejectedEdits{0};
|
||||
QString m_lastInfoMessage;
|
||||
|
||||
QStringList m_availableConfigurations;
|
||||
QString m_currentConfiguration;
|
||||
|
||||
QStringList m_availableAgentRoles;
|
||||
QString m_currentAgentRole;
|
||||
|
||||
ChatCompressor *m_chatCompressor;
|
||||
AgentRoleController *m_agentRoleController;
|
||||
ChatConfigurationController *m_configurationController;
|
||||
FileEditController *m_fileEditController;
|
||||
InputTokenCounter *m_tokenCounter;
|
||||
ChatHistoryStore *m_historyStore;
|
||||
mutable QPointer<SessionFileRegistry> m_sessionFileRegistry;
|
||||
mutable bool m_sessionFileRegistryResolved = false;
|
||||
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
||||
mutable bool m_skillsManagerResolved = false;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "Logger.hpp"
|
||||
@@ -38,14 +22,6 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
||||
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);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||
@@ -88,7 +64,8 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const 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;
|
||||
messageObj["role"] = static_cast<int>(message.role);
|
||||
@@ -103,6 +80,15 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
|
||||
messageObj["signature"] = message.signature;
|
||||
}
|
||||
|
||||
if (message.role == ChatModel::ChatRole::Tool) {
|
||||
if (!message.toolName.isEmpty())
|
||||
messageObj["toolName"] = message.toolName;
|
||||
if (!message.toolArguments.isEmpty())
|
||||
messageObj["toolArguments"] = message.toolArguments;
|
||||
if (!message.toolResult.isEmpty())
|
||||
messageObj["toolResult"] = message.toolResult;
|
||||
}
|
||||
|
||||
if (!message.attachments.isEmpty()) {
|
||||
QJsonArray attachmentsArray;
|
||||
for (const auto &attachment : message.attachments) {
|
||||
@@ -126,10 +112,22 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
|
||||
messageObj["images"] = imagesArray;
|
||||
}
|
||||
|
||||
if (message.promptTokens > 0 || message.completionTokens > 0) {
|
||||
QJsonObject usageObj;
|
||||
usageObj["promptTokens"] = message.promptTokens;
|
||||
usageObj["completionTokens"] = message.completionTokens;
|
||||
if (message.cachedPromptTokens > 0)
|
||||
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
|
||||
if (message.reasoningTokens > 0)
|
||||
usageObj["reasoningTokens"] = message.reasoningTokens;
|
||||
messageObj["usage"] = usageObj;
|
||||
}
|
||||
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, const QString &chatFilePath)
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(
|
||||
const QJsonObject &json, const QString &chatFilePath)
|
||||
{
|
||||
ChatModel::Message message;
|
||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||
@@ -137,6 +135,9 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
||||
message.id = json["id"].toString();
|
||||
message.isRedacted = json["isRedacted"].toBool(false);
|
||||
message.signature = json["signature"].toString();
|
||||
message.toolName = json["toolName"].toString();
|
||||
message.toolArguments = json["toolArguments"].toObject();
|
||||
message.toolResult = json["toolResult"].toString();
|
||||
|
||||
if (json.contains("attachments")) {
|
||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
||||
@@ -161,6 +162,14 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains("usage")) {
|
||||
const QJsonObject usageObj = json["usage"].toObject();
|
||||
message.promptTokens = usageObj["promptTokens"].toInt();
|
||||
message.completionTokens = usageObj["completionTokens"].toInt();
|
||||
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
|
||||
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -178,7 +187,8 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString
|
||||
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();
|
||||
QVector<ChatModel::Message> messages;
|
||||
@@ -193,7 +203,18 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
|
||||
model->setLoadingFromHistory(true);
|
||||
|
||||
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);
|
||||
if (message.role == ChatModel::ChatRole::Tool) {
|
||||
model->setToolMessageData(
|
||||
message.id, message.toolName, message.toolArguments, message.toolResult);
|
||||
}
|
||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
||||
.arg(message.images.size())
|
||||
.arg(message.isRedacted)
|
||||
@@ -219,7 +240,9 @@ bool ChatSerializer::validateVersion(const QString &version)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -234,7 +257,8 @@ QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||
return QDir(dirPath).filePath(baseName + "_content");
|
||||
}
|
||||
|
||||
bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
|
||||
bool ChatSerializer::saveContentToStorage(
|
||||
const QString &chatFilePath,
|
||||
const QString &fileName,
|
||||
const QString &base64Data,
|
||||
QString &storedPath)
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatUtils.h"
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,32 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatView.hpp"
|
||||
|
||||
#include <QQmlComponent>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickItem>
|
||||
#include <QSettings>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <coreplugin/actionmanager/actionmanager.h>
|
||||
#include <coreplugin/actionmanager/command.h>
|
||||
#include <logger/Logger.hpp>
|
||||
|
||||
#include "ChatRootView.hpp"
|
||||
#include "QodeAssistConstants.hpp"
|
||||
#include "SessionFileRegistry.hpp"
|
||||
#include "sources/skills/SkillsManager.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
|
||||
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
|
||||
@@ -35,30 +27,65 @@ constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::Win
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatView::ChatView()
|
||||
: m_isPin(false)
|
||||
ChatView::ChatView(
|
||||
QQmlEngine *engine,
|
||||
SessionFileRegistry *sessionFileRegistry,
|
||||
Skills::SkillsManager *skillsManager)
|
||||
: QQuickView{engine, nullptr}
|
||||
, m_isPin(false)
|
||||
{
|
||||
setTitle("QodeAssist Chat");
|
||||
engine()->rootContext()->setContextProperty("_chatview", this);
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
/// @note setup quick view content
|
||||
{
|
||||
auto context = new QQmlContext{engine, this};
|
||||
context->setContextProperty("_chatview", this);
|
||||
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
|
||||
context->setContextProperty("skillsManager", skillsManager);
|
||||
|
||||
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
|
||||
auto rootItem = component->create(context);
|
||||
|
||||
setContent(component->url(), component, rootItem);
|
||||
}
|
||||
|
||||
if (auto rootView = qobject_cast<ChatRootView *>(rootObject())) {
|
||||
connect(
|
||||
rootView,
|
||||
&ChatRootView::closeHostRequested,
|
||||
this,
|
||||
&QWindow::close,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
setResizeMode(QQuickView::SizeRootObjectToView);
|
||||
setMinimumSize({400, 300});
|
||||
setFlags(baseFlags);
|
||||
|
||||
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
|
||||
m_closeShortcut = new QShortcut(action->keySequence(), this);
|
||||
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
|
||||
|
||||
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
|
||||
if (m_closeShortcut) {
|
||||
m_closeShortcut->setKey(action->keySequence());
|
||||
}
|
||||
bindCommandShortcut("QodeAssist.CloseChatView", [this] { close(); });
|
||||
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE, [this] {
|
||||
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
|
||||
});
|
||||
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_CLEAR_SESSION, [this] {
|
||||
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
||||
});
|
||||
}
|
||||
|
||||
restoreSettings();
|
||||
}
|
||||
|
||||
void ChatView::bindCommandShortcut(Utils::Id commandId,
|
||||
const std::function<void()> &onActivated)
|
||||
{
|
||||
auto command = Core::ActionManager::command(commandId);
|
||||
if (!command)
|
||||
return;
|
||||
|
||||
auto shortcut = new QShortcut(command->keySequence(), this);
|
||||
connect(shortcut, &QShortcut::activated, this, onActivated);
|
||||
connect(command, &Core::Command::keySequenceChanged, shortcut, [command, shortcut]() {
|
||||
shortcut->setKey(command->keySequence());
|
||||
});
|
||||
}
|
||||
|
||||
void ChatView::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
saveSettings();
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <utils/id.h>
|
||||
|
||||
#include <QQuickView>
|
||||
#include <QShortcut>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class SessionFileRegistry;
|
||||
|
||||
class ChatView : public QQuickView
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
|
||||
public:
|
||||
ChatView();
|
||||
ChatView(
|
||||
QQmlEngine *engine,
|
||||
SessionFileRegistry *sessionFileRegistry,
|
||||
Skills::SkillsManager *skillsManager);
|
||||
|
||||
bool isPin() const;
|
||||
void setIsPin(bool newIsPin);
|
||||
@@ -43,9 +40,9 @@ protected:
|
||||
private:
|
||||
void saveSettings();
|
||||
void restoreSettings();
|
||||
void bindCommandShortcut(Utils::Id commandId, const std::function<void()> &onActivated);
|
||||
|
||||
bool m_isPin;
|
||||
QShortcut *m_closeShortcut;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,34 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ChatWidget.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickItem>
|
||||
|
||||
#include <coreplugin/icontext.h>
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include "QodeAssistConstants.hpp"
|
||||
#include "SessionFileRegistry.hpp"
|
||||
#include "sources/skills/SkillsManager.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ChatWidget::ChatWidget(QWidget *parent)
|
||||
: QQuickWidget(parent)
|
||||
ChatWidget::ChatWidget(
|
||||
QQmlEngine *engine,
|
||||
SessionFileRegistry *sessionFileRegistry,
|
||||
Skills::SkillsManager *skillsManager,
|
||||
QWidget *parent)
|
||||
: QQuickWidget{engine, parent}
|
||||
{
|
||||
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||
/// @note setup quick view content
|
||||
{
|
||||
auto context = new QQmlContext{engine, this};
|
||||
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
|
||||
context->setContextProperty("skillsManager", skillsManager);
|
||||
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
|
||||
auto rootItem = component->create(context);
|
||||
|
||||
setContent(component->url(), component, rootItem);
|
||||
}
|
||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
|
||||
auto ideContext = new Core::IContext{this};
|
||||
ideContext->setWidget(this);
|
||||
ideContext->setContext(Core::Context{Constants::QODE_ASSIST_CHAT_CONTEXT});
|
||||
Core::ICore::addContextObject(ideContext);
|
||||
}
|
||||
|
||||
void ChatWidget::clear()
|
||||
@@ -40,4 +52,35 @@ void ChatWidget::scrollToBottom()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
||||
}
|
||||
|
||||
void ChatWidget::focusInput()
|
||||
{
|
||||
setFocus(Qt::OtherFocusReason);
|
||||
QMetaObject::invokeMethod(rootObject(), "focusInput");
|
||||
}
|
||||
|
||||
bool ChatWidget::isChatFocused() const
|
||||
{
|
||||
return hasFocus() || (rootObject() && rootObject()->hasActiveFocus());
|
||||
}
|
||||
|
||||
void ChatWidget::sendMessage()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
|
||||
}
|
||||
|
||||
void ChatWidget::clearSession()
|
||||
{
|
||||
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
||||
}
|
||||
|
||||
ChatWidget *ChatWidget::focusedInstance()
|
||||
{
|
||||
for (QWidget *widget = QApplication::focusWidget(); widget;
|
||||
widget = widget->parentWidget()) {
|
||||
if (auto chatWidget = qobject_cast<ChatWidget *>(widget))
|
||||
return chatWidget;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtQuickWidgets/QtQuickWidgets>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class SessionFileRegistry;
|
||||
|
||||
class ChatWidget : public QQuickWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ChatWidget(QWidget *parent = nullptr);
|
||||
explicit ChatWidget(
|
||||
QQmlEngine *engine,
|
||||
SessionFileRegistry *sessionFileRegistry,
|
||||
Skills::SkillsManager *skillsManager,
|
||||
QWidget *parent = nullptr);
|
||||
~ChatWidget() = default;
|
||||
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE void scrollToBottom();
|
||||
Q_INVOKABLE void focusInput();
|
||||
|
||||
void sendMessage();
|
||||
void clearSession();
|
||||
|
||||
bool isChatFocused() const;
|
||||
|
||||
static ChatWidget *focusedInstance();
|
||||
|
||||
signals:
|
||||
void clearPressed();
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ClientInterface.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
|
||||
#include <projectexplorer/buildconfiguration.h>
|
||||
#include <projectexplorer/target.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
@@ -28,6 +14,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QMimeDatabase>
|
||||
#include <QRegularExpression>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
@@ -40,26 +27,38 @@
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
|
||||
#include "tools/ReadOriginalHistoryTool.hpp"
|
||||
#include "tools/TodoTool.hpp"
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatSerializer.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProjectSettings.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "RequestConfig.hpp"
|
||||
#include "SkillsSettings.hpp"
|
||||
#include "ToolsSettings.hpp"
|
||||
#include <RulesLoader.hpp>
|
||||
#include <context/ChangesManager.h>
|
||||
#include <sources/skills/SkillsManager.hpp>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
ClientInterface::ClientInterface(
|
||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
, m_promptProvider(promptProvider)
|
||||
, m_chatModel(chatModel)
|
||||
, m_contextManager(new Context::ContextManager(this))
|
||||
{}
|
||||
|
||||
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
|
||||
{
|
||||
m_skillsManager = skillsManager;
|
||||
}
|
||||
|
||||
ClientInterface::~ClientInterface()
|
||||
{
|
||||
cancelRequest();
|
||||
@@ -72,6 +71,11 @@ void ClientInterface::sendMessage(
|
||||
bool useTools,
|
||||
bool useThinking)
|
||||
{
|
||||
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
||||
LOG_MESSAGE("Ignoring empty chat message");
|
||||
return;
|
||||
}
|
||||
|
||||
cancelRequest();
|
||||
m_accumulatedResponses.clear();
|
||||
|
||||
@@ -138,7 +142,7 @@ void ClientInterface::sendMessage(
|
||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||
|
||||
auto providerName = Settings::generalSettings().caProvider();
|
||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (!provider) {
|
||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||
@@ -153,7 +157,7 @@ void ClientInterface::sendMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
LLMCore::ContextData context;
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
const bool isToolsEnabled = useTools;
|
||||
|
||||
@@ -167,7 +171,7 @@ void ClientInterface::sendMessage(
|
||||
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
|
||||
}
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
|
||||
if (project) {
|
||||
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
||||
@@ -182,7 +186,7 @@ void ClientInterface::sendMessage(
|
||||
}
|
||||
|
||||
QString projectRules
|
||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
|
||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
|
||||
@@ -191,19 +195,89 @@ void ClientInterface::sendMessage(
|
||||
systemPrompt += QString("\n# No active project in IDE");
|
||||
}
|
||||
|
||||
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
||||
QStringList projectSkillDirs;
|
||||
if (project) {
|
||||
Settings::ProjectSettings projectSettings(project);
|
||||
projectSkillDirs = Settings::SkillsSettings::splitLines(
|
||||
projectSettings.projectSkillDirs());
|
||||
}
|
||||
m_skillsManager->configure(
|
||||
project ? project->projectDirectory().toFSPathString() : QString(),
|
||||
Settings::SkillsSettings::splitPaths(
|
||||
Settings::skillsSettings().globalSkillRoots()),
|
||||
projectSkillDirs);
|
||||
|
||||
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
||||
if (!alwaysOnSkills.isEmpty())
|
||||
systemPrompt += QString("\n\n") + alwaysOnSkills;
|
||||
|
||||
const QString skillsCatalog = m_skillsManager->catalogText();
|
||||
if (!skillsCatalog.isEmpty())
|
||||
systemPrompt += QString("\n\n") + skillsCatalog;
|
||||
|
||||
static const QRegularExpression skillCommand(
|
||||
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
||||
QStringList invokedSkillNames;
|
||||
auto skillMatch = skillCommand.globalMatch(message);
|
||||
while (skillMatch.hasNext()) {
|
||||
const QString skillName = skillMatch.next().captured(1);
|
||||
if (invokedSkillNames.contains(skillName))
|
||||
continue;
|
||||
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
||||
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
||||
invokedSkillNames << skillName;
|
||||
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
|
||||
.arg(invokedSkill->name, invokedSkill->body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!linkedFiles.isEmpty()) {
|
||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||
}
|
||||
context.systemPrompt = systemPrompt;
|
||||
}
|
||||
|
||||
QVector<LLMCore::Message> messages;
|
||||
const bool toolHistory = promptTemplate->supportsToolHistory();
|
||||
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
int toolCallMsgIdx = -1;
|
||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
||||
if (msg.role == ChatModel::ChatRole::Tool) {
|
||||
if (!toolHistory || msg.toolName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LLMCore::Message apiMessage;
|
||||
if (toolCallMsgIdx < 0) {
|
||||
PluginLLMCore::Message assistantCall;
|
||||
assistantCall.role = "assistant";
|
||||
messages.append(assistantCall);
|
||||
toolCallMsgIdx = messages.size() - 1;
|
||||
}
|
||||
|
||||
PluginLLMCore::ToolCall call;
|
||||
call.id = msg.id;
|
||||
call.name = msg.toolName;
|
||||
call.arguments = msg.toolArguments;
|
||||
messages[toolCallMsgIdx].toolCalls.append(call);
|
||||
|
||||
PluginLLMCore::Message toolResult;
|
||||
toolResult.role = "tool";
|
||||
toolResult.toolCallId = msg.id;
|
||||
toolResult.toolName = msg.toolName;
|
||||
toolResult.content = msg.toolResult;
|
||||
messages.append(toolResult);
|
||||
continue;
|
||||
}
|
||||
|
||||
toolCallMsgIdx = -1;
|
||||
|
||||
if (msg.role == ChatModel::ChatRole::FileEdit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginLLMCore::Message apiMessage;
|
||||
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
||||
apiMessage.content = msg.content;
|
||||
|
||||
@@ -223,7 +297,8 @@ void ClientInterface::sendMessage(
|
||||
apiMessage.isRedacted = msg.isRedacted;
|
||||
apiMessage.signature = msg.signature;
|
||||
|
||||
if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
|
||||
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
||||
auto apiImages = loadImagesFromStorage(msg.images);
|
||||
if (!apiImages.isEmpty()) {
|
||||
apiMessage.images = apiImages;
|
||||
@@ -233,112 +308,107 @@ void ClientInterface::sendMessage(
|
||||
messages.append(apiMessage);
|
||||
}
|
||||
|
||||
if (!imageFiles.isEmpty() && !provider->supportImage()) {
|
||||
if (!imageFiles.isEmpty()
|
||||
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
|
||||
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
||||
.arg(provider->name(), QString::number(imageFiles.size())));
|
||||
}
|
||||
|
||||
context.history = messages;
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::Chat;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3")
|
||||
.arg(
|
||||
Settings::generalSettings().caUrl(),
|
||||
Settings::generalSettings().caModel(),
|
||||
stream));
|
||||
} else {
|
||||
config.url
|
||||
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
||||
config.providerRequest
|
||||
= {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
}
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||
|
||||
config.apiKey = provider->apiKey();
|
||||
|
||||
config.provider->prepareRequest(
|
||||
config.providerRequest,
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
context,
|
||||
LLMCore::RequestType::Chat,
|
||||
PluginLLMCore::RequestType::Chat,
|
||||
useTools,
|
||||
useThinking);
|
||||
|
||||
QString requestId = QUuid::createUuid().toString();
|
||||
QJsonObject request{{"id", requestId}};
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
|
||||
emit requestStarted(requestId);
|
||||
provider->client()->setMaxToolContinuations(
|
||||
Settings::toolsSettings().maxToolContinuations());
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::partialResponseReceived,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::chunkReceived,
|
||||
this,
|
||||
&ClientInterface::handlePartialResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&ClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&ClientInterface::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&ClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::toolExecutionStarted,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::toolStarted,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionStarted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::toolExecutionCompleted,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::toolResultReady,
|
||||
this,
|
||||
&ClientInterface::handleToolExecutionCompleted,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::continuationStarted,
|
||||
this,
|
||||
&ClientInterface::handleCleanAccumulatedData,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::thinkingBlockReceived,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::thinkingBlockReceived,
|
||||
this,
|
||||
&ClientInterface::handleThinkingBlockReceived,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::redactedThinkingBlockReceived,
|
||||
this,
|
||||
&ClientInterface::handleRedactedThinkingBlockReceived,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||
QJsonObject request{{"id", requestId}};
|
||||
|
||||
if (provider->supportsTools() && provider->toolsManager()) {
|
||||
provider->toolsManager()->setCurrentSessionId(m_chatFilePath);
|
||||
m_activeRequests[requestId] = {request, provider, !toolHistory};
|
||||
|
||||
emit requestStarted(requestId);
|
||||
|
||||
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
|
||||
provider->toolsManager()->tool("read_original_history"))) {
|
||||
historyTool->setCurrentSessionId(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClientInterface::clearMessages()
|
||||
{
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (provider && !m_chatFilePath.isEmpty() && provider->supportsTools()
|
||||
if (provider && !m_chatFilePath.isEmpty()
|
||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
provider->toolsManager()->clearTodoSession(m_chatFilePath);
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
todoTool->clearSession(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
m_chatModel->clear();
|
||||
@@ -346,7 +416,7 @@ void ClientInterface::clearMessages()
|
||||
|
||||
void ClientInterface::cancelRequest()
|
||||
{
|
||||
QSet<LLMCore::Provider *> providers;
|
||||
QSet<PluginLLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
@@ -354,7 +424,7 @@ void ClientInterface::cancelRequest()
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider, nullptr, this, nullptr);
|
||||
disconnect(provider->client(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
@@ -366,6 +436,7 @@ void ClientInterface::cancelRequest()
|
||||
|
||||
m_activeRequests.clear();
|
||||
m_accumulatedResponses.clear();
|
||||
m_awaitingContinuation.clear();
|
||||
|
||||
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||
}
|
||||
@@ -432,6 +503,12 @@ void ClientInterface::handlePartialResponse(const QString &requestId, const QStr
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
if (m_awaitingContinuation.remove(requestId)) {
|
||||
m_accumulatedResponses[requestId].clear();
|
||||
LOG_MESSAGE(
|
||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
m_accumulatedResponses[requestId] += partialText;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
@@ -462,12 +539,32 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
||||
+ ": " + finalText);
|
||||
emit messageReceivedCompletely();
|
||||
|
||||
if (it != m_activeRequests.end()) {
|
||||
m_activeRequests.erase(it);
|
||||
}
|
||||
if (m_accumulatedResponses.contains(requestId)) {
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
}
|
||||
m_awaitingContinuation.remove(requestId);
|
||||
}
|
||||
|
||||
void ClientInterface::handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId))
|
||||
return;
|
||||
if (!info.usage)
|
||||
return;
|
||||
|
||||
const auto &u = *info.usage;
|
||||
m_chatModel->setMessageUsage(
|
||||
requestId, u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
||||
|
||||
emit messageUsageReceived(
|
||||
u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
||||
|
||||
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||
.arg(requestId)
|
||||
.arg(u.promptTokens)
|
||||
.arg(u.completionTokens)
|
||||
.arg(u.cachedPromptTokens)
|
||||
.arg(u.reasoningTokens));
|
||||
}
|
||||
|
||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
@@ -479,18 +576,9 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin
|
||||
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
||||
emit errorOccurred(error);
|
||||
|
||||
if (it != m_activeRequests.end()) {
|
||||
m_activeRequests.erase(it);
|
||||
}
|
||||
if (m_accumulatedResponses.contains(requestId)) {
|
||||
m_accumulatedResponses.remove(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
|
||||
{
|
||||
m_accumulatedResponses[requestId].clear();
|
||||
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||
m_awaitingContinuation.remove(requestId);
|
||||
}
|
||||
|
||||
void ClientInterface::handleThinkingBlockReceived(
|
||||
@@ -501,30 +589,36 @@ void ClientInterface::handleThinkingBlockReceived(
|
||||
return;
|
||||
}
|
||||
|
||||
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
||||
}
|
||||
|
||||
void ClientInterface::handleRedactedThinkingBlockReceived(
|
||||
const QString &requestId, const QString &signature)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId)) {
|
||||
if (m_awaitingContinuation.remove(requestId)) {
|
||||
m_accumulatedResponses[requestId].clear();
|
||||
LOG_MESSAGE(
|
||||
QString("Ignoring redacted thinking block for non-chat request: %1").arg(requestId));
|
||||
return;
|
||||
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||
}
|
||||
|
||||
if (thinking.isEmpty()) {
|
||||
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
||||
} else {
|
||||
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientInterface::handleToolExecutionStarted(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &arguments)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId)) {
|
||||
const auto requestIt = m_activeRequests.constFind(requestId);
|
||||
if (requestIt == m_activeRequests.constEnd()) {
|
||||
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
||||
return;
|
||||
}
|
||||
|
||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
||||
if (requestIt->dropPreToolText) {
|
||||
m_chatModel->dropTrailingAssistantMessage(requestId);
|
||||
}
|
||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
|
||||
m_awaitingContinuation.insert(requestId);
|
||||
}
|
||||
|
||||
void ClientInterface::handleToolExecutionCompleted(
|
||||
@@ -588,10 +682,10 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
||||
return imageData.toBase64();
|
||||
}
|
||||
|
||||
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||
const QList<ChatModel::ImageAttachment> &storedImages) const
|
||||
{
|
||||
QVector<LLMCore::ImageAttachment> apiImages;
|
||||
QVector<PluginLLMCore::ImageAttachment> apiImages;
|
||||
|
||||
for (const auto &storedImage : storedImages) {
|
||||
QString base64Data
|
||||
@@ -601,7 +695,7 @@ QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||
continue;
|
||||
}
|
||||
|
||||
LLMCore::ImageAttachment apiImage;
|
||||
PluginLLMCore::ImageAttachment apiImage;
|
||||
apiImage.data = base64Data;
|
||||
apiImage.mediaType = storedImage.mediaType;
|
||||
apiImage.isUrl = false;
|
||||
@@ -616,10 +710,15 @@ void ClientInterface::setChatFilePath(const QString &filePath)
|
||||
{
|
||||
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
|
||||
if (provider && provider->supportsTools() && provider->toolsManager()) {
|
||||
provider->toolsManager()->clearTodoSession(m_chatFilePath);
|
||||
if (provider
|
||||
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||
&& provider->toolsManager()) {
|
||||
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||
provider->toolsManager()->tool("todo_tool"))) {
|
||||
todoTool->clearSession(m_chatFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Provider.hpp"
|
||||
#include "llmcore/IPromptProvider.hpp"
|
||||
#include "pluginllmcore/IPromptProvider.hpp"
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <context/ContextManager.hpp>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ClientInterface : public QObject
|
||||
@@ -36,9 +26,11 @@ class ClientInterface : public QObject
|
||||
|
||||
public:
|
||||
explicit ClientInterface(
|
||||
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||
~ClientInterface();
|
||||
|
||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
||||
|
||||
void sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
@@ -57,17 +49,21 @@ signals:
|
||||
void errorOccurred(const QString &error);
|
||||
void messageReceivedCompletely();
|
||||
void requestStarted(const QString &requestId);
|
||||
void messageUsageReceived(
|
||||
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
||||
|
||||
private slots:
|
||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
void handleCleanAccumulatedData(const QString &requestId);
|
||||
void handleThinkingBlockReceived(
|
||||
const QString &requestId, const QString &thinking, const QString &signature);
|
||||
void handleRedactedThinkingBlockReceived(const QString &requestId, const QString &signature);
|
||||
void handleToolExecutionStarted(
|
||||
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
const QString &toolName,
|
||||
const QJsonObject &arguments);
|
||||
void handleToolExecutionCompleted(
|
||||
const QString &requestId,
|
||||
const QString &toolId,
|
||||
@@ -82,21 +78,24 @@ private:
|
||||
bool isImageFile(const QString &filePath) const;
|
||||
QString getMediaTypeForImage(const QString &filePath) const;
|
||||
QString encodeImageToBase64(const QString &filePath) const;
|
||||
QVector<LLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
||||
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
||||
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
PluginLLMCore::Provider *provider;
|
||||
bool dropPreToolText = false;
|
||||
};
|
||||
|
||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
||||
QString m_chatFilePath;
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
QHash<QString, QString> m_accumulatedResponses;
|
||||
QSet<QString> m_awaitingContinuation;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
|
||||
334
ChatView/FileEditController.cpp
Normal file
334
ChatView/FileEditController.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "FileEditController.hpp"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include "ChatModel.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "context/ChangesManager.h"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
{
|
||||
auto &changes = Context::ChangesManager::instance();
|
||||
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
||||
updateStats();
|
||||
});
|
||||
connect(&changes, &Context::ChangesManager::fileEditApplied, this, [this](const QString &) {
|
||||
updateStats();
|
||||
});
|
||||
connect(&changes, &Context::ChangesManager::fileEditRejected, this, [this](const QString &) {
|
||||
updateStats();
|
||||
});
|
||||
connect(&changes, &Context::ChangesManager::fileEditUndone, this, [this](const QString &) {
|
||||
updateStats();
|
||||
});
|
||||
connect(&changes, &Context::ChangesManager::fileEditArchived, this, [this](const QString &) {
|
||||
updateStats();
|
||||
});
|
||||
}
|
||||
|
||||
void FileEditController::setCurrentRequestId(const QString &requestId)
|
||||
{
|
||||
if (!m_currentRequestId.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentRequestId));
|
||||
}
|
||||
|
||||
m_currentRequestId = requestId;
|
||||
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
|
||||
updateStats();
|
||||
}
|
||||
|
||||
void FileEditController::clearCurrentRequestId()
|
||||
{
|
||||
m_currentRequestId.clear();
|
||||
updateStats();
|
||||
}
|
||||
|
||||
int FileEditController::totalEdits() const
|
||||
{
|
||||
return m_totalEdits;
|
||||
}
|
||||
|
||||
int FileEditController::appliedEdits() const
|
||||
{
|
||||
return m_appliedEdits;
|
||||
}
|
||||
|
||||
int FileEditController::pendingEdits() const
|
||||
{
|
||||
return m_pendingEdits;
|
||||
}
|
||||
|
||||
int FileEditController::rejectedEdits() const
|
||||
{
|
||||
return m_rejectedEdits;
|
||||
}
|
||||
|
||||
void FileEditController::applyFileEdit(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||
emit infoMessage(QString("File edit applied successfully"));
|
||||
updateFileEditStatus(editId, "applied");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
emit errorOccurred(
|
||||
edit.statusMessage.isEmpty()
|
||||
? QString("Failed to apply file edit")
|
||||
: QString("Failed to apply file edit: %1").arg(edit.statusMessage));
|
||||
}
|
||||
}
|
||||
|
||||
void FileEditController::rejectFileEdit(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||
emit infoMessage(QString("File edit rejected"));
|
||||
updateFileEditStatus(editId, "rejected");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
emit errorOccurred(
|
||||
edit.statusMessage.isEmpty()
|
||||
? QString("Failed to reject file edit")
|
||||
: QString("Failed to reject file edit: %1").arg(edit.statusMessage));
|
||||
}
|
||||
}
|
||||
|
||||
void FileEditController::undoFileEdit(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||
emit infoMessage(QString("File edit undone successfully"));
|
||||
updateFileEditStatus(editId, "rejected");
|
||||
} else {
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
emit errorOccurred(
|
||||
edit.statusMessage.isEmpty()
|
||||
? QString("Failed to undo file edit")
|
||||
: QString("Failed to undo file edit: %1").arg(edit.statusMessage));
|
||||
}
|
||||
}
|
||||
|
||||
void FileEditController::openFileEditInEditor(const QString &editId)
|
||||
{
|
||||
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
|
||||
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
if (edit.editId.isEmpty()) {
|
||||
emit errorOccurred(QString("File edit not found: %1").arg(editId));
|
||||
return;
|
||||
}
|
||||
|
||||
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
|
||||
|
||||
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
|
||||
if (!editor) {
|
||||
emit errorOccurred(QString("Failed to open file in editor: %1").arg(edit.filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
|
||||
if (textEditor && textEditor->editorWidget()) {
|
||||
QTextDocument *doc = textEditor->editorWidget()->document();
|
||||
if (doc) {
|
||||
QString currentContent = doc->toPlainText();
|
||||
int position = -1;
|
||||
|
||||
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
||||
position = currentContent.indexOf(edit.newContent);
|
||||
} else if (!edit.oldContent.isEmpty()) {
|
||||
position = currentContent.indexOf(edit.oldContent);
|
||||
}
|
||||
|
||||
if (position >= 0) {
|
||||
QTextCursor cursor(doc);
|
||||
cursor.setPosition(position);
|
||||
textEditor->editorWidget()->setTextCursor(cursor);
|
||||
textEditor->editorWidget()->centerCursor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||
}
|
||||
|
||||
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
|
||||
{
|
||||
auto messages = m_chatModel->getChatHistory();
|
||||
for (int i = 0; i < messages.size(); ++i) {
|
||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
||||
QString content = messages[i].content;
|
||||
|
||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||
int markerPos = content.indexOf(marker);
|
||||
|
||||
QString jsonStr = content;
|
||||
if (markerPos >= 0) {
|
||||
jsonStr = content.mid(markerPos + marker.length());
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||
if (doc.isObject()) {
|
||||
QJsonObject obj = doc.object();
|
||||
obj["status"] = status;
|
||||
|
||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||
if (!edit.statusMessage.isEmpty()) {
|
||||
obj["status_message"] = edit.statusMessage;
|
||||
}
|
||||
|
||||
QString updatedContent = marker
|
||||
+ QString::fromUtf8(
|
||||
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateStats();
|
||||
}
|
||||
|
||||
void FileEditController::applyAllForCurrentMessage()
|
||||
{
|
||||
if (m_currentRequestId.isEmpty()) {
|
||||
emit errorOccurred(QString("No active message with file edits"));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentRequestId));
|
||||
|
||||
QString errorMsg;
|
||||
bool success = Context::ChangesManager::instance()
|
||||
.reapplyAllEditsForRequest(m_currentRequestId, &errorMsg);
|
||||
|
||||
if (success) {
|
||||
emit infoMessage(QString("All file edits applied successfully"));
|
||||
} else {
|
||||
emit errorOccurred(
|
||||
errorMsg.isEmpty()
|
||||
? QString("Failed to apply some file edits")
|
||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
||||
}
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Applied) {
|
||||
updateFileEditStatus(edit.editId, "applied");
|
||||
}
|
||||
}
|
||||
|
||||
updateStats();
|
||||
}
|
||||
|
||||
void FileEditController::undoAllForCurrentMessage()
|
||||
{
|
||||
if (m_currentRequestId.isEmpty()) {
|
||||
emit errorOccurred(QString("No active message with file edits"));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentRequestId));
|
||||
|
||||
QString errorMsg;
|
||||
bool success = Context::ChangesManager::instance()
|
||||
.undoAllEditsForRequest(m_currentRequestId, &errorMsg);
|
||||
|
||||
if (success) {
|
||||
emit infoMessage(QString("All file edits undone successfully"));
|
||||
} else {
|
||||
emit errorOccurred(
|
||||
errorMsg.isEmpty()
|
||||
? QString("Failed to undo some file edits")
|
||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
||||
}
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||
for (const auto &edit : edits) {
|
||||
if (edit.status == Context::ChangesManager::Rejected) {
|
||||
updateFileEditStatus(edit.editId, "rejected");
|
||||
}
|
||||
}
|
||||
|
||||
updateStats();
|
||||
}
|
||||
|
||||
void FileEditController::updateStats()
|
||||
{
|
||||
if (m_currentRequestId.isEmpty()) {
|
||||
if (m_totalEdits != 0 || m_appliedEdits != 0 || m_pendingEdits != 0
|
||||
|| m_rejectedEdits != 0) {
|
||||
m_totalEdits = 0;
|
||||
m_appliedEdits = 0;
|
||||
m_pendingEdits = 0;
|
||||
m_rejectedEdits = 0;
|
||||
emit statsChanged();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||
|
||||
int total = edits.size();
|
||||
int applied = 0;
|
||||
int pending = 0;
|
||||
int rejected = 0;
|
||||
|
||||
for (const auto &edit : edits) {
|
||||
switch (edit.status) {
|
||||
case Context::ChangesManager::Applied:
|
||||
applied++;
|
||||
break;
|
||||
case Context::ChangesManager::Pending:
|
||||
pending++;
|
||||
break;
|
||||
case Context::ChangesManager::Rejected:
|
||||
rejected++;
|
||||
break;
|
||||
case Context::ChangesManager::Archived:
|
||||
total--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
if (m_totalEdits != total) {
|
||||
m_totalEdits = total;
|
||||
changed = true;
|
||||
}
|
||||
if (m_appliedEdits != applied) {
|
||||
m_appliedEdits = applied;
|
||||
changed = true;
|
||||
}
|
||||
if (m_pendingEdits != pending) {
|
||||
m_pendingEdits = pending;
|
||||
changed = true;
|
||||
}
|
||||
if (m_rejectedEdits != rejected) {
|
||||
m_rejectedEdits = rejected;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
LOG_MESSAGE(
|
||||
QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
||||
.arg(total)
|
||||
.arg(applied)
|
||||
.arg(pending)
|
||||
.arg(rejected));
|
||||
emit statsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
53
ChatView/FileEditController.hpp
Normal file
53
ChatView/FileEditController.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel;
|
||||
|
||||
class FileEditController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
|
||||
|
||||
void setCurrentRequestId(const QString &requestId);
|
||||
void clearCurrentRequestId();
|
||||
|
||||
int totalEdits() const;
|
||||
int appliedEdits() const;
|
||||
int pendingEdits() const;
|
||||
int rejectedEdits() const;
|
||||
|
||||
void applyFileEdit(const QString &editId);
|
||||
void rejectFileEdit(const QString &editId);
|
||||
void undoFileEdit(const QString &editId);
|
||||
void openFileEditInEditor(const QString &editId);
|
||||
|
||||
void applyAllForCurrentMessage();
|
||||
void undoAllForCurrentMessage();
|
||||
void updateStats();
|
||||
|
||||
signals:
|
||||
void statsChanged();
|
||||
void infoMessage(const QString &message);
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
QString m_currentRequestId;
|
||||
int m_totalEdits{0};
|
||||
int m_appliedEdits{0};
|
||||
int m_pendingEdits{0};
|
||||
int m_rejectedEdits{0};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "FileItem.hpp"
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
426
ChatView/FileMentionItem.cpp
Normal file
426
ChatView/FileMentionItem.cpp
Normal 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 ¤tQuery,
|
||||
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
|
||||
70
ChatView/FileMentionItem.hpp
Normal file
70
ChatView/FileMentionItem.hpp
Normal 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 ¤tQuery,
|
||||
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
|
||||
183
ChatView/InputTokenCounter.cpp
Normal file
183
ChatView/InputTokenCounter.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "InputTokenCounter.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "ChatAssistantSettings.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
#include "GeneralSettings.hpp"
|
||||
#include "Logger.hpp"
|
||||
#include "ProvidersManager.hpp"
|
||||
#include "context/ContextManager.hpp"
|
||||
#include "context/TokenUtils.hpp"
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
InputTokenCounter::InputTokenCounter(
|
||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_chatModel(chatModel)
|
||||
, m_contextManager(contextManager)
|
||||
{
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
connect(
|
||||
&settings.useSystemPrompt,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&InputTokenCounter::recompute);
|
||||
connect(
|
||||
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
|
||||
connect(
|
||||
&settings.enableChatTools,
|
||||
&Utils::BaseAspect::changed,
|
||||
this,
|
||||
&InputTokenCounter::recompute);
|
||||
|
||||
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
});
|
||||
|
||||
rewireToolsChangedConnection();
|
||||
recompute();
|
||||
}
|
||||
|
||||
int InputTokenCounter::inputTokens() const
|
||||
{
|
||||
return m_inputTokens;
|
||||
}
|
||||
|
||||
void InputTokenCounter::setMessage(const QString &message)
|
||||
{
|
||||
m_messageTokens = Context::TokenUtils::estimateTokens(message);
|
||||
recompute();
|
||||
}
|
||||
|
||||
void InputTokenCounter::setAttachments(const QStringList &attachments)
|
||||
{
|
||||
m_attachments = attachments;
|
||||
recompute();
|
||||
}
|
||||
|
||||
void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
||||
{
|
||||
m_linkedFiles = linkedFiles;
|
||||
recompute();
|
||||
}
|
||||
|
||||
void InputTokenCounter::rewireToolsChangedConnection()
|
||||
{
|
||||
if (m_toolsChangedConn)
|
||||
QObject::disconnect(m_toolsChangedConn);
|
||||
m_toolsChangedConn = {};
|
||||
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||
if (!provider)
|
||||
return;
|
||||
auto *tm = provider->toolsManager();
|
||||
if (!tm)
|
||||
return;
|
||||
|
||||
m_toolsChangedConn = connect(
|
||||
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
||||
}
|
||||
|
||||
void InputTokenCounter::recompute()
|
||||
{
|
||||
int inputTokens = m_messageTokens;
|
||||
auto &settings = Settings::chatAssistantSettings();
|
||||
|
||||
if (settings.useSystemPrompt()) {
|
||||
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
||||
}
|
||||
|
||||
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
|
||||
int imageTokens = 0;
|
||||
for (const QString &p : paths) {
|
||||
if (Context::TokenUtils::isImageFilePath(p))
|
||||
imageTokens += Context::TokenUtils::estimateImageAttachmentTokens(p);
|
||||
else
|
||||
textPaths.append(p);
|
||||
}
|
||||
return imageTokens;
|
||||
};
|
||||
|
||||
if (!m_attachments.isEmpty()) {
|
||||
QStringList textPaths;
|
||||
inputTokens += splitImageEstimate(m_attachments, textPaths);
|
||||
if (!textPaths.isEmpty()) {
|
||||
auto attachFiles = m_contextManager->getContentFiles(textPaths);
|
||||
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_linkedFiles.isEmpty()) {
|
||||
QStringList textPaths;
|
||||
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
|
||||
if (!textPaths.isEmpty()) {
|
||||
auto linkFiles = m_contextManager->getContentFiles(textPaths);
|
||||
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
||||
}
|
||||
}
|
||||
|
||||
const auto &history = m_chatModel->getChatHistory();
|
||||
for (const auto &message : history) {
|
||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
||||
inputTokens += 4; // + role
|
||||
}
|
||||
|
||||
if (settings.enableChatTools()) {
|
||||
const auto providerName = Settings::generalSettings().caProvider();
|
||||
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
||||
providerName)) {
|
||||
if (auto *tm = provider->toolsManager()) {
|
||||
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
||||
if (!toolDefs.isEmpty()) {
|
||||
const QByteArray serialized
|
||||
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
||||
inputTokens += static_cast<int>(serialized.size() / 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
||||
emit inputTokensChanged();
|
||||
}
|
||||
|
||||
void InputTokenCounter::recordSent()
|
||||
{
|
||||
m_lastSentEstimate = m_calibrationFactor > 0.0
|
||||
? static_cast<int>(m_inputTokens / m_calibrationFactor)
|
||||
: m_inputTokens;
|
||||
}
|
||||
|
||||
void InputTokenCounter::recordServerUsage(int promptTokens)
|
||||
{
|
||||
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
|
||||
return;
|
||||
|
||||
const double rawFactor
|
||||
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
|
||||
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
|
||||
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped;
|
||||
|
||||
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
|
||||
.arg(promptTokens)
|
||||
.arg(m_lastSentEstimate)
|
||||
.arg(rawFactor, 0, 'f', 3)
|
||||
.arg(m_calibrationFactor, 0, 'f', 3));
|
||||
|
||||
recompute();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
53
ChatView/InputTokenCounter.hpp
Normal file
53
ChatView/InputTokenCounter.hpp
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
namespace QodeAssist::Context {
|
||||
class ContextManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
class ChatModel;
|
||||
|
||||
class InputTokenCounter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
InputTokenCounter(
|
||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr);
|
||||
|
||||
int inputTokens() const;
|
||||
|
||||
void setMessage(const QString &message);
|
||||
void setAttachments(const QStringList &attachments);
|
||||
void setLinkedFiles(const QStringList &linkedFiles);
|
||||
void recompute();
|
||||
|
||||
void recordSent();
|
||||
void recordServerUsage(int promptTokens);
|
||||
|
||||
signals:
|
||||
void inputTokensChanged();
|
||||
|
||||
private:
|
||||
void rewireToolsChangedConnection();
|
||||
|
||||
ChatModel *m_chatModel;
|
||||
Context::ContextManager *m_contextManager;
|
||||
QMetaObject::Connection m_toolsChangedConn;
|
||||
|
||||
QStringList m_attachments;
|
||||
QStringList m_linkedFiles;
|
||||
int m_messageTokens{0};
|
||||
int m_inputTokens{0};
|
||||
int m_lastSentEstimate{0};
|
||||
double m_calibrationFactor{1.0};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
67
ChatView/SessionFileRegistry.cpp
Normal file
67
ChatView/SessionFileRegistry.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "SessionFileRegistry.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
bool SessionFileRegistry::isLocked(const QString &path) const
|
||||
{
|
||||
return !path.isEmpty() && m_lockedPaths.contains(path);
|
||||
}
|
||||
|
||||
bool SessionFileRegistry::lock(const QString &path)
|
||||
{
|
||||
if (path.isEmpty() || m_lockedPaths.contains(path)) {
|
||||
return false;
|
||||
}
|
||||
m_lockedPaths.insert(path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionFileRegistry::release(const QString &path)
|
||||
{
|
||||
m_lockedPaths.remove(path);
|
||||
}
|
||||
|
||||
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
||||
{
|
||||
m_pendingChatFile = path;
|
||||
}
|
||||
|
||||
QString SessionFileRegistry::takePendingChatFile()
|
||||
{
|
||||
return std::exchange(m_pendingChatFile, QString{});
|
||||
}
|
||||
|
||||
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
||||
{
|
||||
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
|
||||
return desiredPath;
|
||||
}
|
||||
|
||||
const QFileInfo info(desiredPath);
|
||||
const QString dir = info.path();
|
||||
const QString base = info.completeBaseName();
|
||||
const QString suffix = info.suffix();
|
||||
|
||||
for (int counter = 2;; ++counter) {
|
||||
QString candidate = dir + '/' + base + '_' + QString::number(counter);
|
||||
if (!suffix.isEmpty()) {
|
||||
candidate += '.' + suffix;
|
||||
}
|
||||
if (!m_lockedPaths.contains(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
38
ChatView/SessionFileRegistry.hpp
Normal file
38
ChatView/SessionFileRegistry.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
// Shared registry of chat session (autosave) file paths that are currently held by a live
|
||||
// chat instance. Lets every chat view — bottom pane, navigation panel, editor split — claim
|
||||
// a unique history file so two sessions never autosave into the same path.
|
||||
class SessionFileRegistry : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SessionFileRegistry(QObject *parent = nullptr);
|
||||
|
||||
bool isLocked(const QString &path) const;
|
||||
bool lock(const QString &path);
|
||||
void release(const QString &path);
|
||||
|
||||
QString uniqueFreePath(const QString &desiredPath) const;
|
||||
|
||||
// Handoff slot for relocating a live chat between hosts (split <-> window): the source
|
||||
// chat stores its history file here, the freshly created host picks it up exactly once.
|
||||
void setPendingChatFile(const QString &path);
|
||||
QString takePendingChatFile();
|
||||
|
||||
private:
|
||||
QSet<QString> m_lockedPaths;
|
||||
QString m_pendingChatFile;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Chat
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@@ -78,7 +62,22 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
|
||||
QoABusyOverlay {
|
||||
id: compressingOverlay
|
||||
|
||||
z: 50
|
||||
|
||||
anchors.fill: mainColumn
|
||||
anchors.topMargin: topBar.height
|
||||
anchors.bottomMargin: bottomBar.height
|
||||
|
||||
active: root.isCompressing
|
||||
text: qsTr("Compressing chat…")
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
@@ -88,14 +87,24 @@ ChatRootView {
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height + 10
|
||||
|
||||
isCompressing: root.isCompressing
|
||||
saveButton.onClicked: root.showSaveDialog()
|
||||
loadButton.onClicked: root.showLoadDialog()
|
||||
clearButton.onClicked: root.clearChat()
|
||||
compressButton.onClicked: compressConfirmDialog.open()
|
||||
cancelCompressButton.onClicked: root.cancelCompression()
|
||||
tokensBadge {
|
||||
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||
readonly property int sessionCached: root.chatModel.sessionCachedPromptTokens
|
||||
text: sessionCached > 0
|
||||
? qsTr("next ~%1 · session ↑%2 ↓%3 ↻%4")
|
||||
.arg(root.inputTokensCount)
|
||||
.arg(root.chatModel.sessionPromptTokens)
|
||||
.arg(root.chatModel.sessionCompletionTokens)
|
||||
.arg(sessionCached)
|
||||
: qsTr("next ~%1 · session ↑%2 ↓%3")
|
||||
.arg(root.inputTokensCount)
|
||||
.arg(root.chatModel.sessionPromptTokens)
|
||||
.arg(root.chatModel.sessionCompletionTokens)
|
||||
ToolTip.text: sessionCached > 0
|
||||
? qsTr("next request (estimate) · session prompt ↑ / completion ↓ / cached ↻ (provider cache hits)")
|
||||
: qsTr("next request (estimate) · session prompt ↑ / completion ↓")
|
||||
}
|
||||
recentPath {
|
||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||
@@ -107,6 +116,17 @@ ChatRootView {
|
||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
||||
}
|
||||
relocateButton {
|
||||
ToolTip.text: (typeof _chatview !== 'undefined')
|
||||
? qsTr("Move this chat to an editor split")
|
||||
: qsTr("Move this chat to a separate window")
|
||||
onClicked: {
|
||||
if (typeof _chatview !== 'undefined')
|
||||
root.relocateToSplit()
|
||||
else
|
||||
root.relocateToWindow()
|
||||
}
|
||||
}
|
||||
toolsButton {
|
||||
checked: root.useTools
|
||||
onCheckedChanged: {
|
||||
@@ -263,6 +283,7 @@ ChatRootView {
|
||||
id: chatItemInstance
|
||||
|
||||
width: parent.width
|
||||
chatViewport: chatListView
|
||||
msgModel: root.chatModel.processMessageContent(model.content)
|
||||
messageAttachments: model.attachments
|
||||
messageImages: model.images
|
||||
@@ -274,12 +295,20 @@ ChatRootView {
|
||||
codeFontSize: root.codeFontSize
|
||||
textFontSize: root.textFontSize
|
||||
textFormat: root.textFormat
|
||||
promptTokens: model.promptTokens || 0
|
||||
completionTokens: model.completionTokens || 0
|
||||
cachedPromptTokens: model.cachedPromptTokens || 0
|
||||
reasoningTokens: model.reasoningTokens || 0
|
||||
|
||||
onResetChatToMessage: function(idx) {
|
||||
messageInput.text = model.content
|
||||
messageInput.cursorPosition = model.content.length
|
||||
root.chatModel.resetModelTo(idx)
|
||||
}
|
||||
|
||||
onOpenFileRequested: function(filePath) {
|
||||
root.openFileInEditor(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +397,68 @@ 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)
|
||||
skillCommandPopup.dismiss()
|
||||
return
|
||||
}
|
||||
}
|
||||
fileMentionPopup.dismiss()
|
||||
|
||||
const slashIndex = textBefore.lastIndexOf('/')
|
||||
if (slashIndex >= 0) {
|
||||
const beforeSlash = slashIndex === 0
|
||||
? ' '
|
||||
: textBefore.charAt(slashIndex - 1)
|
||||
const skillQuery = textBefore.substring(slashIndex + 1)
|
||||
if ((beforeSlash === ' ' || beforeSlash === '\n')
|
||||
&& /^[a-z0-9-]*$/.test(skillQuery)) {
|
||||
skillCommandPopup.updateSearch(skillQuery)
|
||||
return
|
||||
}
|
||||
}
|
||||
skillCommandPopup.dismiss()
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
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
|
||||
}
|
||||
} else if (skillCommandPopup.visible) {
|
||||
if (event.key === Qt.Key_Down) {
|
||||
skillCommandPopup.moveDown()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
skillCommandPopup.moveUp()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.applySkillSelection()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Escape) {
|
||||
skillCommandPopup.dismiss()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -458,12 +548,16 @@ ChatRootView {
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
isCompressing: root.isCompressing
|
||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||
: root.cancelRequest()
|
||||
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
||||
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||
sendButton.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")
|
||||
: qsTr("Stop")
|
||||
compressButton.onClicked: compressConfirmDialog.open()
|
||||
cancelCompressButton.onClicked: root.cancelCompression()
|
||||
syncOpenFiles {
|
||||
checked: root.isSyncOpenFiles
|
||||
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
|
||||
@@ -474,18 +568,6 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
id: sendMessageShortcut
|
||||
|
||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
||||
root.sendChatMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
root.clearMessages()
|
||||
root.clearAttachmentFiles()
|
||||
@@ -496,9 +578,40 @@ ChatRootView {
|
||||
Qt.callLater(chatListView.positionViewAtEnd)
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
messageInput.forceActiveFocus()
|
||||
}
|
||||
|
||||
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 applySkillSelection() {
|
||||
const name = skillCommandPopup.currentName()
|
||||
if (name === "")
|
||||
return
|
||||
const cursorPos = messageInput.cursorPosition
|
||||
const textBefore = messageInput.text.substring(0, cursorPos)
|
||||
const slashIndex = textBefore.lastIndexOf('/')
|
||||
if (slashIndex < 0)
|
||||
return
|
||||
const before = messageInput.text.substring(0, slashIndex)
|
||||
const after = messageInput.text.substring(cursorPos)
|
||||
const token = '/' + name + ' '
|
||||
messageInput.text = before + token + after
|
||||
messageInput.cursorPosition = before.length + token.length
|
||||
skillCommandPopup.dismiss()
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
root.sendMessage(messageInput.text)
|
||||
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||
messageInput.text = ""
|
||||
fileMentionPopup.clearMentions()
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
@@ -572,9 +685,43 @@ ChatRootView {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
SkillCommandPopup {
|
||||
id: skillCommandPopup
|
||||
|
||||
z: 999
|
||||
width: Math.min(480, root.width - 20)
|
||||
|
||||
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
|
||||
y: view.y - height - 4
|
||||
|
||||
skillProvider: root
|
||||
|
||||
onSelectionRequested: root.applySkillSelection()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
messageInput.forceActiveFocus()
|
||||
focusInput()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import ChatView
|
||||
@@ -46,11 +30,18 @@ Rectangle {
|
||||
property int textFontSize: Qt.application.font.pointSize
|
||||
property int codeFontSize: Qt.application.font.pointSize
|
||||
property int textFormat: 0
|
||||
property Flickable chatViewport: null
|
||||
|
||||
property bool isUserMessage: false
|
||||
property int messageIndex: -1
|
||||
|
||||
property int promptTokens: 0
|
||||
property int completionTokens: 0
|
||||
property int cachedPromptTokens: 0
|
||||
property int reasoningTokens: 0
|
||||
|
||||
signal resetChatToMessage(int index)
|
||||
signal openFileRequested(string filePath)
|
||||
|
||||
height: msgColumn.implicitHeight + 10
|
||||
radius: 8
|
||||
@@ -150,6 +141,39 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: usageBadge
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
spacing: 8
|
||||
visible: !root.isUserMessage
|
||||
&& (root.promptTokens > 0 || root.completionTokens > 0)
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Text {
|
||||
text: root.cachedPromptTokens > 0
|
||||
? qsTr("↑ %1 (cached %2)").arg(root.promptTokens).arg(root.cachedPromptTokens)
|
||||
: qsTr("↑ %1").arg(root.promptTokens)
|
||||
color: palette.placeholderText
|
||||
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
||||
}
|
||||
Text {
|
||||
text: root.reasoningTokens > 0
|
||||
? qsTr("↓ %1 (reasoning %2)").arg(root.completionTokens).arg(root.reasoningTokens)
|
||||
: qsTr("↓ %1").arg(root.completionTokens)
|
||||
color: palette.placeholderText
|
||||
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
||||
}
|
||||
Text {
|
||||
text: qsTr("Σ %1").arg(root.promptTokens + root.completionTokens)
|
||||
color: palette.placeholderText
|
||||
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -180,9 +204,12 @@ Rectangle {
|
||||
onClicked: function() {
|
||||
root.resetChatToMessage(root.messageIndex)
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Reset chat to this message and edit")
|
||||
ToolTip.delay: 500
|
||||
|
||||
QoAToolTip {
|
||||
visible: stopButtonId.hovered
|
||||
text: qsTr("Reset chat to this message and edit")
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
|
||||
component TextComponent : TextBlock {
|
||||
@@ -204,6 +231,15 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
if (link.startsWith("file://")) {
|
||||
var filePath = link.replace(/^file:\/\//, "")
|
||||
root.openFileRequested(filePath)
|
||||
} else {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
|
||||
ChatUtils {
|
||||
id: utils
|
||||
}
|
||||
@@ -224,6 +260,7 @@ Rectangle {
|
||||
language: itemData.language
|
||||
codeFontFamily: root.codeFontFamily
|
||||
codeFontSize: root.codeFontSize
|
||||
viewport: root.chatViewport
|
||||
}
|
||||
|
||||
component AttachmentComponent : Rectangle {
|
||||
@@ -257,33 +294,21 @@ Rectangle {
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||
fileItem.openFileInExternalEditor()
|
||||
} else {
|
||||
fileItem.openFileInEditor()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
attachmentContextMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.visible: containsMouse
|
||||
ToolTip.text: qsTr("Left click: Open in Qt Creator\nRight click: More options")
|
||||
ToolTip.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()
|
||||
QoAToolTip {
|
||||
visible: attachFileMouseArea.containsMouse
|
||||
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +330,7 @@ Rectangle {
|
||||
|
||||
FileItem {
|
||||
id: imageFileItem
|
||||
filePath: itemData.imageUrl ? itemData.imageUrl.toString().replace("file://", "") : ""
|
||||
filePath: itemData.filePath || ""
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -361,33 +386,21 @@ Rectangle {
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||
imageFileItem.openFileInExternalEditor()
|
||||
} else {
|
||||
imageFileItem.openFileInEditor()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
imageContextMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.visible: containsMouse
|
||||
ToolTip.text: qsTr("Left click: Open in System\nRight click: More options")
|
||||
ToolTip.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()
|
||||
QoAToolTip {
|
||||
visible: imageMouseArea.containsMouse
|
||||
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@@ -29,6 +13,7 @@ Rectangle {
|
||||
property string code: ""
|
||||
property string language: ""
|
||||
property bool expanded: false
|
||||
property Flickable viewport: null
|
||||
|
||||
property alias codeFontFamily: codeText.font.family
|
||||
property alias codeFontSize: codeText.font.pointSize
|
||||
@@ -138,7 +123,16 @@ Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 5
|
||||
|
||||
y: 5
|
||||
y: {
|
||||
if (!root.expanded || !root.viewport)
|
||||
return 5
|
||||
const flick = root.viewport
|
||||
const topInContent = root.mapToItem(flick.contentItem, 0, 0).y
|
||||
const topInView = topInContent - flick.contentY
|
||||
const desired = topInView < 0 ? (-topInView + 5) : 5
|
||||
const maxY = Math.max(5, root.height - copyButton.height - 5)
|
||||
return Math.max(5, Math.min(desired, maxY))
|
||||
}
|
||||
text: qsTr("Copy")
|
||||
|
||||
onClicked: {
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
@@ -29,8 +13,6 @@ TextEdit {
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
|
||||
onLinkActivated: (link) => Qt.openUrlExternally(link)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.platform as Platform
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import UIControls
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
@@ -78,9 +63,11 @@ Flow {
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.visible: containsMouse
|
||||
ToolTip.delay: 500
|
||||
ToolTip.text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
|
||||
QoAToolTip {
|
||||
visible: mouse.containsMouse
|
||||
delay: 500
|
||||
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
@@ -31,6 +15,10 @@ Rectangle {
|
||||
property alias attachFiles: attachFilesId
|
||||
property alias attachImages: attachImagesId
|
||||
property alias linkFiles: linkFilesId
|
||||
property alias compressButton: compressButtonId
|
||||
property alias cancelCompressButton: cancelCompressButtonId
|
||||
|
||||
property bool isCompressing: false
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
@@ -49,17 +37,6 @@ Rectangle {
|
||||
|
||||
spacing: 10
|
||||
|
||||
QoAButton {
|
||||
id: sendButtonId
|
||||
|
||||
icon {
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: attachFilesId
|
||||
|
||||
@@ -111,5 +88,66 @@ Rectangle {
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Row {
|
||||
id: compressingRow
|
||||
|
||||
visible: root.isCompressing
|
||||
spacing: 6
|
||||
|
||||
BusyIndicator {
|
||||
id: compressBusyIndicator
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
running: root.isCompressing
|
||||
width: 16
|
||||
height: 16
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Compressing...")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: palette.text
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: cancelCompressButtonId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Cancel")
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
151
ChatView/qml/controls/FileMentionPopup.qml
Normal file
151
ChatView/qml/controls/FileMentionPopup.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
ChatView/qml/controls/SkillCommandPopup.qml
Normal file
125
ChatView/qml/controls/SkillCommandPopup.qml
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// Object exposing Q_INVOKABLE QVariantList searchSkills(query).
|
||||
property var skillProvider: null
|
||||
property var searchResults: []
|
||||
property int currentIndex: 0
|
||||
|
||||
signal selectionRequested()
|
||||
|
||||
visible: searchResults.length > 0
|
||||
height: Math.min(searchResults.length * 40, 40 * 6) + 2
|
||||
|
||||
color: palette.window
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
radius: 4
|
||||
|
||||
function updateSearch(query) {
|
||||
searchResults = skillProvider ? skillProvider.searchSkills(query) : []
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
searchResults = []
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
function moveUp() {
|
||||
if (currentIndex > 0)
|
||||
currentIndex--
|
||||
}
|
||||
|
||||
function moveDown() {
|
||||
if (currentIndex < searchResults.length - 1)
|
||||
currentIndex++
|
||||
}
|
||||
|
||||
function currentName() {
|
||||
if (currentIndex >= 0 && currentIndex < searchResults.length)
|
||||
return searchResults[currentIndex].name
|
||||
return ""
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: listView.positionViewAtIndex(currentIndex, ListView.Contain)
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
model: root.searchResults
|
||||
currentIndex: root.currentIndex
|
||||
clip: true
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: delegateItem
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
width: listView.width
|
||||
height: 40
|
||||
color: index === root.currentIndex
|
||||
? palette.highlight
|
||||
: (hoverArea.containsMouse
|
||||
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
|
||||
: "transparent")
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
anchors.topMargin: 4
|
||||
anchors.bottomMargin: 4
|
||||
spacing: 1
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "/" + delegateItem.modelData.name
|
||||
color: delegateItem.index === root.currentIndex
|
||||
? palette.highlightedText
|
||||
: palette.text
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: delegateItem.modelData.description
|
||||
color: delegateItem.index === root.currentIndex
|
||||
? Qt.rgba(palette.highlightedText.r,
|
||||
palette.highlightedText.g,
|
||||
palette.highlightedText.b, 0.7)
|
||||
: palette.mid
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hoverArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
root.currentIndex = delegateItem.index
|
||||
root.selectionRequested()
|
||||
}
|
||||
onEntered: root.currentIndex = delegateItem.index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
@@ -29,12 +13,11 @@ Rectangle {
|
||||
property alias saveButton: saveButtonId
|
||||
property alias loadButton: loadButtonId
|
||||
property alias clearButton: clearButtonId
|
||||
property alias compressButton: compressButtonId
|
||||
property alias cancelCompressButton: cancelCompressButtonId
|
||||
property alias tokensBadge: tokensBadgeId
|
||||
property alias recentPath: recentPathId
|
||||
property alias openChatHistory: openChatHistoryId
|
||||
property alias pinButton: pinButtonId
|
||||
property alias relocateButton: relocateButtonId
|
||||
property alias contextButton: contextButtonId
|
||||
property alias toolsButton: toolsButtonId
|
||||
property alias thinkingMode: thinkingModeId
|
||||
@@ -42,8 +25,6 @@ Rectangle {
|
||||
property alias configSelector: configSelectorId
|
||||
property alias roleSelector: roleSelector
|
||||
|
||||
property bool isCompressing: false
|
||||
|
||||
color: palette.window.hslLightness > 0.5 ?
|
||||
Qt.darker(palette.window, 1.1) :
|
||||
Qt.lighter(palette.window, 1.1)
|
||||
@@ -81,6 +62,21 @@ Rectangle {
|
||||
: qsTr("Pin chat window to the top")
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: relocateButtonId
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
}
|
||||
|
||||
QoAComboBox {
|
||||
id: configSelectorId
|
||||
|
||||
@@ -258,55 +254,6 @@ Rectangle {
|
||||
|
||||
QoASeparator {}
|
||||
|
||||
QoAButton {
|
||||
id: compressButtonId
|
||||
|
||||
visible: !root.isCompressing
|
||||
|
||||
icon {
|
||||
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
|
||||
height: 15
|
||||
width: 15
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||
}
|
||||
|
||||
Row {
|
||||
id: compressingRow
|
||||
|
||||
visible: root.isCompressing
|
||||
spacing: 6
|
||||
|
||||
BusyIndicator {
|
||||
id: compressBusyIndicator
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
running: root.isCompressing
|
||||
width: 16
|
||||
height: 16
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Compressing...")
|
||||
height: parent.height
|
||||
color: palette.text
|
||||
font.pixelSize: 12
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: cancelCompressButtonId
|
||||
|
||||
text: qsTr("Cancel")
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 250
|
||||
ToolTip.text: qsTr("Cancel compression")
|
||||
}
|
||||
}
|
||||
|
||||
QoAButton {
|
||||
id: contextButtonId
|
||||
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "CodeHandler.hpp"
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
|
||||
@@ -41,7 +25,7 @@ void ConfigurationManager::init()
|
||||
|
||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
||||
{
|
||||
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||
|
||||
if (!templ) {
|
||||
return;
|
||||
@@ -65,7 +49,7 @@ void ConfigurationManager::updateAllTemplateDescriptions()
|
||||
|
||||
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
||||
{
|
||||
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||
|
||||
if (templ->name() == templateAspect.value())
|
||||
return;
|
||||
@@ -86,8 +70,8 @@ void ConfigurationManager::checkAllTemplate()
|
||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_generalSettings(Settings::generalSettings())
|
||||
, m_providersManager(LLMCore::ProvidersManager::instance())
|
||||
, m_templateManger(LLMCore::PromptTemplateManager::instance())
|
||||
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
|
||||
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
|
||||
{}
|
||||
|
||||
void ConfigurationManager::setupConnections()
|
||||
@@ -170,27 +154,25 @@ void ConfigurationManager::selectModel()
|
||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
||||
: m_generalSettings.caUrl.volatileValue();
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
||||
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||
: isQuickRefactor ? m_generalSettings.qrModel
|
||||
: m_generalSettings.caModel;
|
||||
: m_generalSettings.caModel);
|
||||
|
||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||
if (!provider->supportsModelListing()) {
|
||||
m_generalSettings.showModelsNotSupportedDialog(targetSettings);
|
||||
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
|
||||
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto modelList = provider->getInstalledModels(providerUrl);
|
||||
|
||||
provider->getInstalledModels(providerUrl)
|
||||
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
||||
if (modelList.isEmpty()) {
|
||||
m_generalSettings.showModelsNotFoundDialog(targetSettings);
|
||||
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "llmcore/PromptTemplateManager.hpp"
|
||||
#include "llmcore/ProvidersManager.hpp"
|
||||
#include "pluginllmcore/PromptTemplateManager.hpp"
|
||||
#include "pluginllmcore/ProvidersManager.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
namespace QodeAssist {
|
||||
@@ -54,8 +38,8 @@ private:
|
||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
||||
|
||||
Settings::GeneralSettings &m_generalSettings;
|
||||
LLMCore::ProvidersManager &m_providersManager;
|
||||
LLMCore::PromptTemplateManager &m_templateManger;
|
||||
PluginLLMCore::ProvidersManager &m_providersManager;
|
||||
PluginLLMCore::PromptTemplateManager &m_templateManger;
|
||||
|
||||
void setupConnections();
|
||||
};
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "LLMClientInterface.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
@@ -29,16 +14,15 @@
|
||||
#include "logger/Logger.hpp"
|
||||
#include "settings/CodeCompletionSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <llmcore/RulesLoader.hpp>
|
||||
#include <pluginllmcore/RulesLoader.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
LLMClientInterface::LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
LLMCore::IProviderRegistry &providerRegistry,
|
||||
LLMCore::IPromptProvider *promptProvider,
|
||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||
PluginLLMCore::IPromptProvider *promptProvider,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger)
|
||||
: m_generalSettings(generalSettings)
|
||||
@@ -79,16 +63,32 @@ void LLMClientInterface::handleFullResponse(const QString &requestId, const QStr
|
||||
m_performanceLogger.endTimeMeasurement(requestId);
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (!m_activeRequests.contains(requestId) || !info.usage)
|
||||
return;
|
||||
|
||||
const auto &u = *info.usage;
|
||||
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||
.arg(requestId)
|
||||
.arg(u.promptTokens)
|
||||
.arg(u.completionTokens)
|
||||
.arg(u.cachedPromptTokens)
|
||||
.arg(u.reasoningTokens));
|
||||
}
|
||||
|
||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
auto it = m_activeRequests.find(requestId);
|
||||
if (it == m_activeRequests.end())
|
||||
return;
|
||||
|
||||
const RequestContext &ctx = it.value();
|
||||
|
||||
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
||||
|
||||
// Send LSP error response to client
|
||||
const RequestContext &ctx = it.value();
|
||||
QJsonObject response;
|
||||
response["jsonrpc"] = "2.0";
|
||||
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
|
||||
@@ -122,8 +122,6 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
} else if (method == "textDocument/didOpen") {
|
||||
handleTextDocumentDidOpen(request);
|
||||
} else if (method == "getCompletionsCycling") {
|
||||
QString requestId = request["id"].toString();
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
handleCompletion(request);
|
||||
} else if (method == "$/cancelRequest") {
|
||||
handleCancelRequest();
|
||||
@@ -136,7 +134,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
||||
|
||||
void LLMClientInterface::handleCancelRequest()
|
||||
{
|
||||
QSet<LLMCore::Provider *> providers;
|
||||
QSet<PluginLLMCore::Provider *> providers;
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
if (it.value().provider) {
|
||||
providers.insert(it.value().provider);
|
||||
@@ -144,7 +142,7 @@ void LLMClientInterface::handleCancelRequest()
|
||||
}
|
||||
|
||||
for (auto *provider : providers) {
|
||||
disconnect(provider, nullptr, this, nullptr);
|
||||
disconnect(provider->client(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||
@@ -270,39 +268,24 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO refactor to dynamic presets system
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
// TODO refactor networking
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
||||
} else {
|
||||
config.url = QUrl(
|
||||
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
|
||||
config.providerRequest = {{"model", modelName}, {"stream", true}};
|
||||
}
|
||||
config.apiKey = provider->apiKey();
|
||||
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
|
||||
QJsonObject payload{{"model", modelName}, {"stream", true}};
|
||||
|
||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
||||
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
|
||||
if (!stopWords.isEmpty())
|
||||
config.providerRequest["stop"] = stopWords;
|
||||
payload["stop"] = stopWords;
|
||||
|
||||
QString systemPrompt;
|
||||
if (m_completeSettings.useSystemPrompt())
|
||||
systemPrompt.append(
|
||||
m_completeSettings.useUserMessageTemplateForCC()
|
||||
&& promptTemplate->type() == LLMCore::TemplateType::Chat
|
||||
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
|
||||
? m_completeSettings.systemPromptForNonFimModels()
|
||||
: m_completeSettings.systemPrompt());
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules
|
||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions);
|
||||
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
@@ -314,10 +297,10 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
systemPrompt.append(updatedContext.fileContext.value());
|
||||
|
||||
if (m_completeSettings.useOpenFilesContext()) {
|
||||
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
|
||||
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
|
||||
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
||||
if (!updatedContext.filesMetadata) {
|
||||
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
|
||||
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
|
||||
}
|
||||
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
||||
}
|
||||
@@ -328,7 +311,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
|
||||
updatedContext.systemPrompt = systemPrompt;
|
||||
|
||||
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
|
||||
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
|
||||
QString userMessage;
|
||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
||||
userMessage = m_completeSettings.processMessageToFIM(
|
||||
@@ -338,50 +321,45 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
||||
}
|
||||
|
||||
// TODO refactor add message
|
||||
QVector<LLMCore::Message> messages;
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
messages.append({"user", userMessage});
|
||||
updatedContext.history = messages;
|
||||
}
|
||||
|
||||
config.provider->prepareRequest(
|
||||
config.providerRequest,
|
||||
provider->prepareRequest(
|
||||
payload,
|
||||
promptTemplate,
|
||||
updatedContext,
|
||||
LLMCore::RequestType::CodeCompletion,
|
||||
PluginLLMCore::RequestType::CodeCompletion,
|
||||
false,
|
||||
false);
|
||||
|
||||
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
||||
if (!errors.isEmpty()) {
|
||||
QString error = QString("Request validation failed: %1").arg(errors.join("; "));
|
||||
LOG_MESSAGE("Validate errors for request:");
|
||||
LOG_MESSAGES(errors);
|
||||
sendErrorResponse(request, error);
|
||||
return;
|
||||
}
|
||||
|
||||
QString requestId = request["id"].toString();
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&LLMClientInterface::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&LLMClientInterface::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&LLMClientInterface::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
m_performanceLogger.startTimeMeasurement(requestId);
|
||||
}
|
||||
|
||||
LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||
{
|
||||
QJsonObject params = request["params"].toObject();
|
||||
@@ -395,24 +373,12 @@ LLMCore::ContextData LLMClientInterface::prepareContext(
|
||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||
}
|
||||
|
||||
QString LLMClientInterface::endpoint(
|
||||
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
|
||||
QString LLMClientInterface::resolveEndpoint(
|
||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
|
||||
{
|
||||
QString endpoint;
|
||||
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
|
||||
: m_generalSettings.ccEndpointMode.stringValue();
|
||||
if (endpointMode == "Auto") {
|
||||
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
||||
: provider->chatEndpoint();
|
||||
} else if (endpointMode == "Custom") {
|
||||
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||
: m_generalSettings.ccCustomEndpoint();
|
||||
} else if (endpointMode == "FIM") {
|
||||
endpoint = provider->completionEndpoint();
|
||||
} else if (endpointMode == "Chat") {
|
||||
endpoint = provider->chatEndpoint();
|
||||
}
|
||||
return endpoint;
|
||||
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
|
||||
}
|
||||
|
||||
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||
|
||||
@@ -1,33 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <context/ProgrammingLanguage.hpp>
|
||||
#include <llmcore/ContextData.hpp>
|
||||
#include <llmcore/IPromptProvider.hpp>
|
||||
#include <llmcore/IProviderRegistry.hpp>
|
||||
#include <pluginllmcore/ContextData.hpp>
|
||||
#include <pluginllmcore/IPromptProvider.hpp>
|
||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||
#include <logger/IRequestPerformanceLogger.hpp>
|
||||
#include <settings/CodeCompletionSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
@@ -45,8 +30,8 @@ public:
|
||||
LLMClientInterface(
|
||||
const Settings::GeneralSettings &generalSettings,
|
||||
const Settings::CodeCompletionSettings &completeSettings,
|
||||
LLMCore::IProviderRegistry &providerRegistry,
|
||||
LLMCore::IPromptProvider *promptProvider,
|
||||
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||
PluginLLMCore::IPromptProvider *promptProvider,
|
||||
Context::IDocumentReader &documentReader,
|
||||
IRequestPerformanceLogger &performanceLogger);
|
||||
~LLMClientInterface() override;
|
||||
@@ -68,6 +53,8 @@ protected:
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
@@ -82,17 +69,19 @@ private:
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
PluginLLMCore::Provider *provider;
|
||||
};
|
||||
|
||||
LLMCore::ContextData prepareContext(
|
||||
PluginLLMCore::ContextData prepareContext(
|
||||
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
|
||||
|
||||
QString resolveEndpoint(
|
||||
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
|
||||
|
||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||
const Settings::GeneralSettings &m_generalSettings;
|
||||
LLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
LLMCore::IProviderRegistry &m_providerRegistry;
|
||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
||||
Context::IDocumentReader &m_documentReader;
|
||||
IRequestPerformanceLogger &m_performanceLogger;
|
||||
QElapsedTimer m_completionTimer;
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
*
|
||||
* Petr Mironychev portions:
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "LLMSuggestion.hpp"
|
||||
#include <texteditor/texteditor.h>
|
||||
@@ -29,55 +9,42 @@
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
static QStringList extractTokens(const QString &str)
|
||||
static bool isClosingTail(const QString &s, int from)
|
||||
{
|
||||
QStringList tokens;
|
||||
QString currentToken;
|
||||
for (const QChar &ch : str) {
|
||||
if (ch.isLetterOrNumber() || ch == '_') {
|
||||
currentToken += ch;
|
||||
} else {
|
||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
||||
tokens.append(currentToken);
|
||||
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||
for (int i = from; i < s.size(); ++i) {
|
||||
const QChar c = s.at(i);
|
||||
if (!c.isSpace() && !closeChars.contains(c))
|
||||
return false;
|
||||
}
|
||||
currentToken.clear();
|
||||
}
|
||||
}
|
||||
if (!currentToken.isEmpty() && currentToken.length() > 1) {
|
||||
tokens.append(currentToken);
|
||||
}
|
||||
return tokens;
|
||||
return true;
|
||||
}
|
||||
|
||||
int LLMSuggestion::calculateReplaceLength(const QString &suggestion,
|
||||
const QString &rightText,
|
||||
const QString &entireLine)
|
||||
int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText)
|
||||
{
|
||||
if (rightText.isEmpty()) {
|
||||
if (rightText.isEmpty())
|
||||
return 0;
|
||||
|
||||
const int maxN = qMin(suggestion.size(), rightText.size());
|
||||
int lcp = 0;
|
||||
while (lcp < maxN && suggestion.at(lcp) == rightText.at(lcp))
|
||||
++lcp;
|
||||
|
||||
if (lcp > 0) {
|
||||
if (isClosingTail(rightText, lcp))
|
||||
return rightText.size();
|
||||
return lcp;
|
||||
}
|
||||
|
||||
QString structuralChars = "{}[]()<>;,";
|
||||
bool hasStructuralOverlap = false;
|
||||
for (const QChar &ch : structuralChars) {
|
||||
if (suggestion.contains(ch) && rightText.contains(ch)) {
|
||||
hasStructuralOverlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isClosingTail(rightText, 0))
|
||||
return 0;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||
int i = suggestion.size() - 1;
|
||||
while (i >= 0 && suggestion.at(i).isSpace())
|
||||
--i;
|
||||
if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i)))
|
||||
return rightText.size();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -102,10 +69,9 @@ LLMSuggestion::LLMSuggestion(
|
||||
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||
|
||||
QString suggestionText = data.text;
|
||||
QString entireLine = blockText;
|
||||
|
||||
if (!suggestionText.contains('\n')) {
|
||||
int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine);
|
||||
int replaceLength = calculateReplaceLength(suggestionText, rightText);
|
||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
||||
|
||||
QString displayText = leftText + suggestionText + remainingRightText;
|
||||
@@ -115,7 +81,7 @@ LLMSuggestion::LLMSuggestion(
|
||||
QString firstLine = suggestionText.left(firstLineEnd);
|
||||
QString restOfCompletion = suggestionText.mid(firstLineEnd);
|
||||
|
||||
int replaceLength = calculateReplaceLength(firstLine, rightText, entireLine);
|
||||
int replaceLength = calculateReplaceLength(firstLine, rightText);
|
||||
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
|
||||
|
||||
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
|
||||
@@ -167,9 +133,8 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||
if (startPos == 0) {
|
||||
QTextBlock currentBlock = cursor.block();
|
||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||
QString entireLine = currentBlock.text();
|
||||
|
||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
||||
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||
|
||||
if (replaceLength > 0) {
|
||||
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||
@@ -220,9 +185,7 @@ bool LLMSuggestion::apply()
|
||||
QString text = currentData.text;
|
||||
|
||||
QTextBlock currentBlock = cursor.block();
|
||||
QString textBeforeCursor = currentBlock.text().left(cursor.positionInBlock());
|
||||
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||
QString entireLine = currentBlock.text();
|
||||
|
||||
QTextCursor editCursor = cursor;
|
||||
editCursor.beginEditBlock();
|
||||
@@ -232,7 +195,7 @@ bool LLMSuggestion::apply()
|
||||
QString firstLine = text.left(firstLineEnd);
|
||||
QString restOfText = text.mid(firstLineEnd);
|
||||
|
||||
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine);
|
||||
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor);
|
||||
|
||||
if (replaceLength > 0) {
|
||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||
@@ -241,7 +204,7 @@ bool LLMSuggestion::apply()
|
||||
|
||||
editCursor.insertText(firstLine + restOfText);
|
||||
} else {
|
||||
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
|
||||
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||
|
||||
if (replaceLength > 0) {
|
||||
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -42,8 +42,6 @@ public:
|
||||
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
|
||||
bool apply() override;
|
||||
|
||||
static int calculateReplaceLength(const QString &suggestion,
|
||||
const QString &rightText,
|
||||
const QString &entireLine);
|
||||
static int calculateReplaceLength(const QString &suggestion, const QString &rightText);
|
||||
};
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.9.5",
|
||||
"Version" : "0.9.14",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -54,6 +54,90 @@ using namespace Core;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
namespace {
|
||||
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
|
||||
{
|
||||
return Utils::Text::Position{pos.line, pos.column};
|
||||
}
|
||||
|
||||
bool isIdentifierChar(QChar c)
|
||||
{
|
||||
return c.isLetterOrNumber() || c == QLatin1Char('_');
|
||||
}
|
||||
|
||||
bool isInsideIdentifier(const QTextCursor &cursor)
|
||||
{
|
||||
const QTextBlock block = cursor.block();
|
||||
const int col = cursor.positionInBlock();
|
||||
const QString text = block.text();
|
||||
if (col <= 0 || col > text.size())
|
||||
return false;
|
||||
if (!isIdentifierChar(text.at(col - 1)))
|
||||
return false;
|
||||
return col < text.size() && isIdentifierChar(text.at(col));
|
||||
}
|
||||
|
||||
bool isAfterMemberAccess(const QTextCursor &cursor)
|
||||
{
|
||||
const QTextBlock block = cursor.block();
|
||||
const int col = cursor.positionInBlock();
|
||||
const QString text = block.text();
|
||||
if (col <= 0)
|
||||
return false;
|
||||
|
||||
int i = col - 1;
|
||||
while (i >= 0 && isIdentifierChar(text.at(i)))
|
||||
--i;
|
||||
|
||||
if (i < 0)
|
||||
return false;
|
||||
|
||||
const QChar c = text.at(i);
|
||||
if (c == QLatin1Char('.'))
|
||||
return true;
|
||||
if (c == QLatin1Char('>') && i >= 1 && text.at(i - 1) == QLatin1Char('-'))
|
||||
return true;
|
||||
if (c == QLatin1Char(':') && i >= 1 && text.at(i - 1) == QLatin1Char(':'))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isFreshIndentedLine(const QTextCursor &cursor)
|
||||
{
|
||||
const QTextBlock block = cursor.block();
|
||||
const int col = cursor.positionInBlock();
|
||||
if (col == 0)
|
||||
return false;
|
||||
const QString leftText = block.text().left(col);
|
||||
for (const QChar &ch : leftText) {
|
||||
if (!ch.isSpace())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isAfterEagerTrigger(const QTextCursor &cursor)
|
||||
{
|
||||
const QTextBlock block = cursor.block();
|
||||
const int col = cursor.positionInBlock();
|
||||
const QString text = block.text();
|
||||
int i = col - 1;
|
||||
while (i >= 0 && text.at(i).isSpace())
|
||||
--i;
|
||||
if (i < 0)
|
||||
return false;
|
||||
const QChar c = text.at(i);
|
||||
return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(',')
|
||||
|| c == QLatin1Char('=') || c == QLatin1Char('[') || c == QLatin1Char(';')
|
||||
|| c == QLatin1Char(':') || c == QLatin1Char('>');
|
||||
}
|
||||
|
||||
bool isManualMode()
|
||||
{
|
||||
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||
: LanguageClient::Client(clientInterface)
|
||||
, m_llmClient(clientInterface)
|
||||
@@ -69,10 +153,6 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||
|
||||
m_typingTimer.start();
|
||||
|
||||
m_hintHideTimer.setSingleShot(true);
|
||||
m_hintHideTimer.setInterval(Settings::codeCompletionSettings().hintHideTimeout());
|
||||
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
|
||||
|
||||
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
||||
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||
}
|
||||
@@ -108,6 +188,9 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||
return;
|
||||
|
||||
if (isManualMode())
|
||||
return;
|
||||
|
||||
auto project = ProjectManager::projectForFile(document->filePath());
|
||||
if (!isEnabled(project))
|
||||
return;
|
||||
@@ -131,38 +214,29 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||
if (charsRemoved > 0 || charsAdded <= 0) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
// 0 = Hint-based, 1 = Automatic
|
||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
||||
if (triggerMode != 1) {
|
||||
m_hintHideTimer.stop();
|
||||
m_hintHandler.hideHint();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor cursor = widget->textCursor();
|
||||
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||
QString lastChar = cursor.selectedText();
|
||||
const QString lastChar = cursor.selectedText();
|
||||
if (lastChar.isEmpty())
|
||||
return;
|
||||
|
||||
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
|
||||
const QChar lastCh = lastChar[0];
|
||||
if (lastCh == QLatin1Char('\n') || lastCh == QChar::ParagraphSeparator
|
||||
|| lastCh == QChar::LineSeparator) {
|
||||
m_recentCharCount = 0;
|
||||
m_typingTimer.restart();
|
||||
// 0 = Hint-based, 1 = Automatic
|
||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
||||
if (triggerMode != 1) {
|
||||
m_hintHideTimer.stop();
|
||||
m_hintHandler.hideHint();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool isSpaceOrTab = lastChar[0].isSpace();
|
||||
bool ignoreWhitespace
|
||||
const bool isSpaceOrTab = lastCh.isSpace();
|
||||
const bool ignoreWhitespace
|
||||
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
|
||||
|
||||
if (!ignoreWhitespace || !isSpaceOrTab) {
|
||||
if (!ignoreWhitespace || !isSpaceOrTab)
|
||||
m_recentCharCount += charsAdded;
|
||||
}
|
||||
|
||||
if (m_typingTimer.elapsed()
|
||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
||||
@@ -170,13 +244,7 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||
m_typingTimer.restart();
|
||||
}
|
||||
|
||||
// 0 = Hint-based, 1 = Automatic
|
||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
||||
if (triggerMode == 1) {
|
||||
handleAutoRequestTrigger(widget, charsAdded, isSpaceOrTab);
|
||||
} else {
|
||||
handleHintBasedTrigger(widget, charsAdded, isSpaceOrTab, cursor);
|
||||
}
|
||||
handleAutoRequestTrigger(widget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -205,11 +273,9 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
|
||||
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
|
||||
return;
|
||||
|
||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
||||
|
||||
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 0) {
|
||||
const auto &settings = Settings::codeCompletionSettings();
|
||||
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||
editor->abortAssist();
|
||||
}
|
||||
|
||||
const FilePath filePath = editor->textDocument()->filePath();
|
||||
GetCompletionRequest request{
|
||||
@@ -270,33 +336,29 @@ void QodeAssistClient::requestQuickRefactor(
|
||||
|
||||
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
||||
{
|
||||
if (m_runningRequests.contains(editor)) {
|
||||
if (Settings::codeCompletionSettings().cancelOnInput())
|
||||
cancelRunningRequest(editor);
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = m_scheduledRequests.find(editor);
|
||||
if (it == m_scheduledRequests.end()) {
|
||||
auto timer = new QTimer(this);
|
||||
timer->setSingleShot(true);
|
||||
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
||||
if (editor
|
||||
&& editor->textCursor().position()
|
||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
|
||||
&& m_recentCharCount
|
||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
|
||||
if (!editor || m_runningRequests.contains(editor))
|
||||
return;
|
||||
if (editor->textCursor().position()
|
||||
!= m_scheduledRequests[editor]->property("cursorPosition").toInt())
|
||||
return;
|
||||
requestCompletions(editor);
|
||||
});
|
||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||
delete m_scheduledRequests.take(editor);
|
||||
cancelRunningRequest(editor);
|
||||
});
|
||||
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
|
||||
cancelRunningRequest(editor);
|
||||
// 0 = Hint-based, 1 = Automatic
|
||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
||||
if (triggerMode != 1) {
|
||||
m_hintHideTimer.stop();
|
||||
m_hintHandler.hideHint();
|
||||
}
|
||||
});
|
||||
it = m_scheduledRequests.insert(editor, timer);
|
||||
}
|
||||
|
||||
@@ -307,11 +369,9 @@ void QodeAssistClient::handleCompletions(
|
||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||
{
|
||||
m_progressHandler.hideProgress();
|
||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
||||
|
||||
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 1) {
|
||||
const auto &settings = Settings::codeCompletionSettings();
|
||||
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||
editor->abortAssist();
|
||||
}
|
||||
|
||||
if (response.error()) {
|
||||
log(*response.error());
|
||||
@@ -325,12 +385,25 @@ void QodeAssistClient::handleCompletions(
|
||||
requestPosition = requestParams->position().toPositionInDocument(editor->document());
|
||||
|
||||
const MultiTextCursor cursors = editor->multiTextCursor();
|
||||
if (cursors.hasMultipleCursors())
|
||||
if (cursors.hasMultipleCursors() || cursors.hasSelection())
|
||||
return;
|
||||
|
||||
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition)
|
||||
const int currentPosition = cursors.mainCursor().position();
|
||||
if (requestPosition < 0 || currentPosition < requestPosition)
|
||||
return;
|
||||
|
||||
QString typedSinceRequest;
|
||||
if (currentPosition > requestPosition) {
|
||||
QTextCursor diffCursor(editor->document());
|
||||
diffCursor.setPosition(requestPosition);
|
||||
diffCursor.setPosition(currentPosition, QTextCursor::KeepAnchor);
|
||||
typedSinceRequest = diffCursor.selectedText();
|
||||
if (typedSinceRequest.contains(QChar::ParagraphSeparator)
|
||||
|| typedSinceRequest.contains(QLatin1Char('\n'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
||||
auto isValidCompletion = [](const Completion &completion) {
|
||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||
@@ -338,34 +411,58 @@ void QodeAssistClient::handleCompletions(
|
||||
QList<Completion> completions
|
||||
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
||||
|
||||
QList<Completion> matchedCompletions;
|
||||
matchedCompletions.reserve(completions.size());
|
||||
for (Completion &completion : completions) {
|
||||
const LanguageServerProtocol::Range range = completion.range();
|
||||
if (range.start().line() != range.end().line())
|
||||
continue;
|
||||
|
||||
const QString completionText = completion.text();
|
||||
QString completionText = completion.text();
|
||||
const int end = int(completionText.size()) - 1;
|
||||
int delta = 0;
|
||||
while (delta <= end && completionText[end - delta].isSpace())
|
||||
++delta;
|
||||
|
||||
if (delta > 0)
|
||||
completion.setText(completionText.chopped(delta));
|
||||
completionText.chop(delta);
|
||||
|
||||
if (!typedSinceRequest.isEmpty()) {
|
||||
if (!completionText.startsWith(typedSinceRequest))
|
||||
continue;
|
||||
completionText = completionText.mid(typedSinceRequest.size());
|
||||
if (completionText.isEmpty())
|
||||
continue;
|
||||
}
|
||||
auto suggestions = Utils::transform(completions, [](const Completion &c) {
|
||||
|
||||
completion.setText(completionText);
|
||||
matchedCompletions.append(completion);
|
||||
}
|
||||
|
||||
if (matchedCompletions.isEmpty()) {
|
||||
LOG_MESSAGE("No valid completions received");
|
||||
return;
|
||||
}
|
||||
|
||||
const Text::Position anchor = typedSinceRequest.isEmpty()
|
||||
? Text::Position{}
|
||||
: Text::Position::fromPositionInDocument(editor->document(), currentPosition);
|
||||
const bool useAnchor = !typedSinceRequest.isEmpty();
|
||||
|
||||
auto suggestions = Utils::transform(matchedCompletions,
|
||||
[useAnchor, &anchor](const Completion &c) {
|
||||
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
||||
return Text::Position{pos.line() + 1, pos.character()};
|
||||
};
|
||||
|
||||
if (useAnchor) {
|
||||
return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
|
||||
}
|
||||
|
||||
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
||||
Text::Position pos{toTextPos(c.position())};
|
||||
return TextSuggestion::Data{range, pos, c.text()};
|
||||
});
|
||||
|
||||
if (completions.isEmpty()) {
|
||||
LOG_MESSAGE("No valid completions received");
|
||||
return;
|
||||
}
|
||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||
}
|
||||
}
|
||||
@@ -376,12 +473,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
|
||||
if (it == m_runningRequests.constEnd())
|
||||
return;
|
||||
m_progressHandler.hideProgress();
|
||||
// 0 = Hint-based, 1 = Automatic
|
||||
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
|
||||
if (triggerMode != 1) {
|
||||
m_hintHideTimer.stop();
|
||||
m_hintHandler.hideHint();
|
||||
}
|
||||
cancelRequest(it->id());
|
||||
m_runningRequests.erase(it);
|
||||
}
|
||||
@@ -423,17 +514,6 @@ void QodeAssistClient::cleanupConnections()
|
||||
m_scheduledRequests.clear();
|
||||
}
|
||||
|
||||
bool QodeAssistClient::isHintVisible() const
|
||||
{
|
||||
return m_hintHandler.isHintVisible();
|
||||
}
|
||||
|
||||
void QodeAssistClient::hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor)
|
||||
{
|
||||
m_hintHandler.hideHint();
|
||||
requestCompletions(editor);
|
||||
}
|
||||
|
||||
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||
{
|
||||
m_progressHandler.hideProgress();
|
||||
@@ -465,13 +545,6 @@ void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
|
||||
{
|
||||
return Utils::Text::Position{pos.line, pos.column};
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
|
||||
{
|
||||
TextEditorWidget *editorWidget = result.editor;
|
||||
@@ -604,58 +677,20 @@ void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor
|
||||
editCursor.endEditBlock();
|
||||
}
|
||||
|
||||
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
|
||||
int charsAdded,
|
||||
bool isSpaceOrTab)
|
||||
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
|
||||
{
|
||||
Q_UNUSED(isSpaceOrTab);
|
||||
const QTextCursor cursor = widget->textCursor();
|
||||
const auto &settings = Settings::codeCompletionSettings();
|
||||
const bool smart = settings.smartContextTrigger();
|
||||
|
||||
if (m_recentCharCount
|
||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
||||
if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
|
||||
return;
|
||||
|
||||
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
|
||||
const int charThreshold = settings.autoCompletionCharThreshold();
|
||||
|
||||
if (eager || m_recentCharCount > charThreshold)
|
||||
scheduleRequest(widget);
|
||||
}
|
||||
}
|
||||
|
||||
void QodeAssistClient::handleHintBasedTrigger(TextEditor::TextEditorWidget *widget,
|
||||
int charsAdded,
|
||||
bool isSpaceOrTab,
|
||||
QTextCursor &cursor)
|
||||
{
|
||||
Q_UNUSED(charsAdded);
|
||||
|
||||
const int hintThreshold = Settings::codeCompletionSettings().hintCharThreshold();
|
||||
if (m_recentCharCount >= hintThreshold && !isSpaceOrTab) {
|
||||
const QRect cursorRect = widget->cursorRect(cursor);
|
||||
QPoint globalPos = widget->viewport()->mapToGlobal(cursorRect.topLeft());
|
||||
QPoint localPos = widget->mapFromGlobal(globalPos);
|
||||
|
||||
int fontSize = widget->font().pixelSize();
|
||||
if (fontSize <= 0) {
|
||||
fontSize = widget->fontMetrics().height();
|
||||
}
|
||||
|
||||
QTextCursor textCursor = widget->textCursor();
|
||||
|
||||
if (m_recentCharCount <= hintThreshold) {
|
||||
textCursor
|
||||
.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, m_recentCharCount);
|
||||
} else {
|
||||
textCursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, hintThreshold);
|
||||
}
|
||||
|
||||
int x = localPos.x() + cursorRect.height();
|
||||
int y = localPos.y() + cursorRect.height() / 4;
|
||||
|
||||
QPoint hintPos(x, y);
|
||||
|
||||
if (!m_hintHandler.isHintVisible()) {
|
||||
m_hintHandler.showHint(widget, hintPos, fontSize);
|
||||
} else {
|
||||
m_hintHandler.updateHintPosition(widget, hintPos);
|
||||
}
|
||||
|
||||
m_hintHideTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||
@@ -667,46 +702,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
|
||||
// Check hint trigger key (0=Space, 1=Ctrl+Space, 2=Alt+Space, 3=Ctrl+Enter, 4=Tab, 5=Enter)
|
||||
if (m_hintHandler.isHintVisible()) {
|
||||
const int triggerKeyIndex = Settings::codeCompletionSettings().hintTriggerKey();
|
||||
bool isMatchingKey = false;
|
||||
const Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
|
||||
|
||||
switch (triggerKeyIndex) {
|
||||
case 0: // Space
|
||||
isMatchingKey = (keyEvent->key() == Qt::Key_Space
|
||||
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
|
||||
break;
|
||||
case 1: // Ctrl+Space
|
||||
isMatchingKey = (keyEvent->key() == Qt::Key_Space
|
||||
&& (modifiers & Qt::ControlModifier));
|
||||
break;
|
||||
case 2: // Alt+Space
|
||||
isMatchingKey = (keyEvent->key() == Qt::Key_Space
|
||||
&& (modifiers & Qt::AltModifier));
|
||||
break;
|
||||
case 3: // Ctrl+Enter
|
||||
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
|
||||
&& (modifiers & Qt::ControlModifier));
|
||||
break;
|
||||
case 4: // Tab
|
||||
isMatchingKey = (keyEvent->key() == Qt::Key_Tab);
|
||||
break;
|
||||
case 5: // Enter
|
||||
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
|
||||
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
|
||||
break;
|
||||
}
|
||||
|
||||
if (isMatchingKey) {
|
||||
m_hintHideTimer.stop();
|
||||
m_hintHandler.hideHint();
|
||||
requestCompletions(editor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyEvent->key() == Qt::Key_Escape) {
|
||||
if (m_runningRequests.contains(editor)) {
|
||||
cancelRunningRequest(editor);
|
||||
@@ -724,8 +719,6 @@ bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||
}
|
||||
|
||||
m_progressHandler.hideProgress();
|
||||
m_hintHideTimer.stop();
|
||||
m_hintHandler.hideHint();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Qt Company Ltd.
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* The Qt Company portions:
|
||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
*
|
||||
* Petr Mironychev portions:
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -32,12 +12,11 @@
|
||||
#include "RefactorSuggestionHoverHandler.hpp"
|
||||
#include "widgets/CompletionProgressHandler.hpp"
|
||||
#include "widgets/CompletionErrorHandler.hpp"
|
||||
#include "widgets/CompletionHintHandler.hpp"
|
||||
#include "widgets/EditorChatButtonHandler.hpp"
|
||||
#include "widgets/RefactorWidgetHandler.hpp"
|
||||
#include <languageclient/client.h>
|
||||
#include <llmcore/IPromptProvider.hpp>
|
||||
#include <llmcore/IProviderRegistry.hpp>
|
||||
#include <pluginllmcore/IPromptProvider.hpp>
|
||||
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
@@ -55,9 +34,6 @@ public:
|
||||
void requestQuickRefactor(
|
||||
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
|
||||
|
||||
bool isHintVisible() const;
|
||||
void hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
@@ -75,8 +51,7 @@ private:
|
||||
void displayRefactoringWidget(const RefactorResult &result);
|
||||
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
||||
|
||||
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
|
||||
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
|
||||
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
|
||||
|
||||
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
||||
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
||||
@@ -85,10 +60,8 @@ private:
|
||||
|
||||
QElapsedTimer m_typingTimer;
|
||||
int m_recentCharCount;
|
||||
QTimer m_hintHideTimer;
|
||||
CompletionProgressHandler m_progressHandler;
|
||||
CompletionErrorHandler m_errorHandler;
|
||||
CompletionHintHandler m_hintHandler;
|
||||
EditorChatButtonHandler m_chatButtonHandler;
|
||||
QuickRefactorHandler *m_refactorHandler{nullptr};
|
||||
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -26,4 +10,15 @@ const char MENU_ID[] = "QodeAssist.Menu";
|
||||
|
||||
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
|
||||
|
||||
const char QODE_ASSIST_CHAT_CONTEXT[] = "QodeAssist.ChatContext";
|
||||
const char QODE_ASSIST_CHAT_NAV_ID[] = "QodeAssistChat";
|
||||
const char QODE_ASSIST_CHAT_EDITOR_ID[] = "QodeAssist.ChatEditor";
|
||||
|
||||
const char QODE_ASSIST_SHOW_CHAT_ACTION[] = "QodeAssist.ShowChatView";
|
||||
const char QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION[] = "QodeAssist.OpenChatWindow";
|
||||
|
||||
const char QODE_ASSIST_CHAT_SEND_MESSAGE[] = "QodeAssist.Chat.SendMessage";
|
||||
const char QODE_ASSIST_CHAT_CLEAR_SESSION[] = "QodeAssist.Chat.ClearSession";
|
||||
const char QODE_ASSIST_CHAT_SHOW_IN_RIGHT[] = "QodeAssist.Chat.ShowInRightSidebar";
|
||||
|
||||
} // namespace QodeAssist::Constants
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,40 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "QuickRefactorHandler.hpp"
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QUuid>
|
||||
|
||||
#include <context/DocumentContextReader.hpp>
|
||||
#include <llmcore/ResponseCleaner.hpp>
|
||||
#include <pluginllmcore/ResponseCleaner.hpp>
|
||||
#include <context/DocumentReaderQtCreator.hpp>
|
||||
#include <context/Utils.hpp>
|
||||
#include <llmcore/PromptTemplateManager.hpp>
|
||||
#include <llmcore/ProvidersManager.hpp>
|
||||
#include <llmcore/RequestConfig.hpp>
|
||||
#include <llmcore/RulesLoader.hpp>
|
||||
#include <pluginllmcore/PromptTemplateManager.hpp>
|
||||
#include <pluginllmcore/ProvidersManager.hpp>
|
||||
#include <pluginllmcore/RulesLoader.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/ChatAssistantSettings.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/QuickRefactorSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
@@ -109,8 +94,8 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
||||
{
|
||||
auto &settings = Settings::generalSettings();
|
||||
|
||||
auto &providerRegistry = LLMCore::ProvidersManager::instance();
|
||||
auto &promptManager = LLMCore::PromptTemplateManager::instance();
|
||||
auto &providerRegistry = PluginLLMCore::ProvidersManager::instance();
|
||||
auto &promptManager = PluginLLMCore::PromptTemplateManager::instance();
|
||||
|
||||
const auto providerName = settings.qrProvider();
|
||||
auto provider = providerRegistry.getProviderByName(providerName);
|
||||
@@ -140,70 +125,64 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
||||
return;
|
||||
}
|
||||
|
||||
LLMCore::LLMConfig config;
|
||||
config.requestType = LLMCore::RequestType::QuickRefactoring;
|
||||
config.provider = provider;
|
||||
config.promptTemplate = promptTemplate;
|
||||
config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint());
|
||||
config.apiKey = provider->apiKey();
|
||||
QJsonObject payload{
|
||||
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
||||
|
||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
||||
QString stream = QString{"streamGenerateContent?alt=sse"};
|
||||
config.url = QUrl(QString("%1/models/%2:%3")
|
||||
.arg(
|
||||
Settings::generalSettings().qrUrl(),
|
||||
Settings::generalSettings().qrModel(),
|
||||
stream));
|
||||
} else {
|
||||
config.url
|
||||
= QString("%1%2").arg(Settings::generalSettings().qrUrl(), provider->chatEndpoint());
|
||||
config.providerRequest
|
||||
= {{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
||||
}
|
||||
|
||||
LLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||
|
||||
bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
||||
provider->prepareRequest(
|
||||
config.providerRequest,
|
||||
payload,
|
||||
promptTemplate,
|
||||
context,
|
||||
LLMCore::RequestType::QuickRefactoring,
|
||||
PluginLLMCore::RequestType::QuickRefactoring,
|
||||
enableTools,
|
||||
enableThinking);
|
||||
|
||||
QString requestId = QUuid::createUuid().toString();
|
||||
m_lastRequestId = requestId;
|
||||
QJsonObject request{{"id", requestId}};
|
||||
provider->client()->setMaxToolContinuations(
|
||||
Settings::toolsSettings().maxToolContinuations());
|
||||
|
||||
m_isRefactoringInProgress = true;
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::fullResponseReceived,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestCompleted,
|
||||
this,
|
||||
&QuickRefactorHandler::handleFullResponse,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(
|
||||
provider,
|
||||
&LLMCore::Provider::requestFailed,
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFinalized,
|
||||
this,
|
||||
&QuickRefactorHandler::handleRequestFinalized,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(
|
||||
provider->client(),
|
||||
&::LLMQore::BaseClient::requestFailed,
|
||||
this,
|
||||
&QuickRefactorHandler::handleRequestFailed,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
provider->sendRequest(requestId, config.url, config.providerRequest);
|
||||
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||
: promptTemplate->endpoint();
|
||||
auto requestId
|
||||
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
||||
m_lastRequestId = requestId;
|
||||
QJsonObject request{{"id", requestId}};
|
||||
|
||||
m_activeRequests[requestId] = {request, provider};
|
||||
}
|
||||
|
||||
LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &instructions)
|
||||
{
|
||||
LLMCore::ContextData context;
|
||||
PluginLLMCore::ContextData context;
|
||||
|
||||
auto textDocument = editor->textDocument();
|
||||
Context::DocumentReaderQtCreator documentReader;
|
||||
@@ -287,10 +266,10 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
|
||||
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
|
||||
|
||||
auto project = LLMCore::RulesLoader::getActiveProject();
|
||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||
if (project) {
|
||||
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
|
||||
project, LLMCore::RulesContext::QuickRefactor);
|
||||
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
||||
project, PluginLLMCore::RulesContext::QuickRefactor);
|
||||
|
||||
if (!projectRules.isEmpty()) {
|
||||
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
|
||||
@@ -368,7 +347,7 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||
|
||||
context.systemPrompt = systemPrompt;
|
||||
|
||||
QVector<LLMCore::Message> messages;
|
||||
QVector<PluginLLMCore::Message> messages;
|
||||
messages.append(
|
||||
{"user",
|
||||
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
|
||||
@@ -387,7 +366,7 @@ void QuickRefactorHandler::handleLLMResponse(
|
||||
|
||||
if (isComplete) {
|
||||
m_isRefactoringInProgress = false;
|
||||
QString cleanedResponse = LLMCore::ResponseCleaner::clean(response);
|
||||
QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response);
|
||||
|
||||
RefactorResult result;
|
||||
result.newText = cleanedResponse;
|
||||
@@ -436,6 +415,22 @@ void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QS
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
||||
{
|
||||
if (requestId != m_lastRequestId || !info.usage)
|
||||
return;
|
||||
|
||||
const auto &u = *info.usage;
|
||||
LOG_MESSAGE(
|
||||
QString("Quick refactor usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
||||
.arg(requestId)
|
||||
.arg(u.promptTokens)
|
||||
.arg(u.completionTokens)
|
||||
.arg(u.cachedPromptTokens)
|
||||
.arg(u.reasoningTokens));
|
||||
}
|
||||
|
||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
||||
{
|
||||
if (requestId == m_lastRequestId) {
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
#include <LLMQore/BaseClient.hpp>
|
||||
#include <texteditor/texteditor.h>
|
||||
#include <utils/textutils.h>
|
||||
|
||||
#include <context/ContextManager.hpp>
|
||||
#include <context/IDocumentReader.hpp>
|
||||
#include <llmcore/ContextData.hpp>
|
||||
#include <llmcore/Provider.hpp>
|
||||
#include <pluginllmcore/ContextData.hpp>
|
||||
#include <pluginllmcore/Provider.hpp>
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
@@ -59,6 +44,8 @@ signals:
|
||||
|
||||
private slots:
|
||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||
void handleRequestFinalized(
|
||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||
|
||||
private:
|
||||
@@ -68,7 +55,7 @@ private:
|
||||
const Utils::Text::Range &range);
|
||||
|
||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
||||
LLMCore::ContextData prepareContext(
|
||||
PluginLLMCore::ContextData prepareContext(
|
||||
TextEditor::TextEditorWidget *editor,
|
||||
const Utils::Text::Range &range,
|
||||
const QString &instructions);
|
||||
@@ -76,7 +63,7 @@ private:
|
||||
struct RequestContext
|
||||
{
|
||||
QJsonObject originalRequest;
|
||||
LLMCore::Provider *provider;
|
||||
PluginLLMCore::Provider *provider;
|
||||
};
|
||||
|
||||
QHash<QString, RequestContext> m_activeRequests;
|
||||
|
||||
167
README.md
167
README.md
@@ -1,10 +1,12 @@
|
||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||

|
||||

|
||||
[](https://discord.gg/BGMkUsXUgf)
|
||||
# QodeAssist — AI coding assistant for Qt Creator
|
||||
|
||||
 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.
|
||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||
[](https://github.com/Palm1r/QodeAssist/releases)
|
||||

|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://discord.gg/BGMkUsXUgf)
|
||||
|
||||
 **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
|
||||
|
||||
⚠️ **Important Notice About Paid Providers**
|
||||
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
||||
@@ -29,13 +31,16 @@
|
||||
|
||||
QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
||||
|
||||
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML
|
||||
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel)
|
||||
- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library
|
||||
- **File Context**: Attach or link files for better AI understanding
|
||||
- **Tool Calling**: AI can read project files, search code, and access diagnostics
|
||||
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more
|
||||
- **Customizable**: Project-specific rules, custom instructions, and extensive model templates
|
||||
- **Code Completion** — intelligent, context-aware suggestions (FIM and chat models) for C++ and QML, with multiline support
|
||||
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
|
||||
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
|
||||
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
|
||||
- **Agent Skills** — reusable folders of specialized instructions loaded on demand; discovered from `.qodeassist/skills/` and `.claude/skills/`, invoked automatically, with `/skill`, or always-on
|
||||
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
|
||||
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
|
||||
- **File Context** — attach, link, or auto-sync open editor files for richer prompts
|
||||
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, any OpenAI-compatible endpoint
|
||||
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
|
||||
|
||||
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
||||
|
||||
@@ -125,18 +130,41 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
|
||||
|
||||
## 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
|
||||
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
||||
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
|
||||
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider (includes Responses API support)
|
||||
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
|
||||
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
|
||||
- **LM Studio** - Local LLM provider
|
||||
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
|
||||
1. **Open QodeAssist Settings**
|
||||
2. **Select a Preset** - Choose from the Quick Setup dropdown:
|
||||
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
|
||||
- **OpenAI** (gpt-5.2-codex)
|
||||
- **Mistral AI** (Codestral 2501)
|
||||
- **Google AI** (Gemini 2.5 Flash)
|
||||
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
||||
|
||||
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
||||
|
||||
### 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
|
||||
|
||||
@@ -159,6 +187,7 @@ For optimal coding assistance, we recommend using these top-tier models:
|
||||
### 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
|
||||
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
|
||||
|
||||
@@ -196,6 +225,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
||||
- Chat history with auto-save and restore
|
||||
- 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
|
||||
- Automatic syncing with open editor files (optional)
|
||||
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
||||
@@ -208,10 +238,72 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
||||
- **[Learn more](docs/quick-refactoring.md)**
|
||||
|
||||
### Tools & Function Calling
|
||||
- Read project files
|
||||
- List and search in project
|
||||
- Access linter/compiler issues
|
||||
- Enabled by default (can be disabled)
|
||||
|
||||
Chat and Quick Refactor can call tools to inspect and modify your project. Each tool can be individually enabled/disabled in settings.
|
||||
|
||||
| Tool | What it does |
|
||||
|------|--------------|
|
||||
| `list_project_files` | List files in the active project(s) |
|
||||
| `find_file` | Find a file by name or partial path |
|
||||
| `read_file` | Read file contents (project or absolute path) |
|
||||
| `search_project` | Grep / symbol search across project sources |
|
||||
| `create_new_file` | Create a new empty file on disk |
|
||||
| `edit_file` | Replace content in a file (old → new) |
|
||||
| `build_project` | Build the active project and return compiler output |
|
||||
| `get_issues_list` | Read current linter / compiler diagnostics |
|
||||
| `execute_terminal_command` | Run a shell command (with confirmation) |
|
||||
| `todo_tool` | Track multi-step task progress during a conversation |
|
||||
|
||||
### Skills
|
||||
|
||||
**Agent Skills** package specialized instructions and workflows into reusable folders the AI loads on demand. QodeAssist implements the open [Agent Skills](https://agentskills.io) format, so skills authored for Claude Code, Cursor, or other agents work as-is.
|
||||
|
||||
A skill is a folder containing a `SKILL.md` file — YAML frontmatter (`name`, `description`) plus Markdown instructions:
|
||||
|
||||
```
|
||||
my-skill/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: What the skill does and when to use it.
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
Step-by-step instructions for the task...
|
||||
```
|
||||
|
||||
**Where skills are discovered:**
|
||||
- **Project skills** — project-relative subdirectories (default `.qodeassist/skills/` and `.claude/skills/`), configured in `Projects → QodeAssist → Skills`. Project skills win over global ones on a name collision.
|
||||
- **Global skills** — absolute directories shared across all projects (default includes `~/.claude/skills/`), configured in `Tools → Options → QodeAssist → Skills`.
|
||||
|
||||
Both settings pages show the list of currently discovered skills.
|
||||
|
||||
**How skills are used in Chat:**
|
||||
- **Automatically** — each skill's name and description is added to the system prompt; when a request matches, the model loads the full instructions via the `load_skill` tool (requires a tool-calling model).
|
||||
- **Explicitly** — type `/` in the chat input and pick a skill from the popup; its instructions are injected into that one message. Works with any model.
|
||||
- **Always-on** — a skill whose frontmatter has `metadata: always-on: "true"` is injected into every chat request automatically.
|
||||
|
||||
Enable or disable the whole feature in `Tools → Options → QodeAssist → Skills`.
|
||||
|
||||
### MCP Server
|
||||
|
||||
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
|
||||
|
||||
- **Enable** in `Tools → Options → QodeAssist → MCP Server`
|
||||
- **Transport**: HTTP + SSE by default; a stdio bridge is provided for clients that only speak stdio (e.g. Claude Desktop)
|
||||
- **Ready-to-copy snippets** for Claude Code, VS Code, and the bridge are available via the "Show connection instructions" button in settings
|
||||
|
||||
### MCP Client Hub
|
||||
|
||||
QodeAssist can also act as an **MCP client**, connecting to external MCP servers and making their tools available to Chat and Quick Refactor alongside the built-in ones.
|
||||
|
||||
- **Configure** servers in `Tools → Options → QodeAssist → MCP Client`
|
||||
- **Transports**: stdio and HTTP/SSE
|
||||
- **Limitation**: authenticated MCP servers (OAuth / token-protected) are **not supported yet** — only servers that accept unauthenticated local connections work for now
|
||||
|
||||
## Context Layers
|
||||
|
||||
@@ -358,7 +450,7 @@ See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](do
|
||||
| Qt Creator Version | QodeAssist Version |
|
||||
|-------------------|-------------------|
|
||||
| 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.0 | 0.5.2 - 0.5.6 |
|
||||
| 15.0.1 | 0.4.8 - 0.5.1 |
|
||||
@@ -393,14 +485,17 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
|
||||
|
||||
## Development Progress
|
||||
|
||||
- [x] Code completion functionality
|
||||
- [x] Chat assistant with multiple panels
|
||||
- [x] Code completion (FIM and chat models)
|
||||
- [x] Chat assistant (side / bottom / detached panels)
|
||||
- [x] Quick refactoring with custom-instructions library
|
||||
- [x] Diff sharing with models
|
||||
- [x] Tools/function calling support
|
||||
- [x] Project-specific rules
|
||||
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
||||
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
|
||||
- [x] Project-specific rules (`.qodeassist/rules/`)
|
||||
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
||||
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
||||
- [ ] Full project source sharing
|
||||
- [ ] Additional provider support
|
||||
- [ ] MCP (Model Context Protocol) support
|
||||
|
||||
## Support the development of QodeAssist
|
||||
If you find QodeAssist helpful, there are several ways you can support the project:
|
||||
@@ -412,6 +507,7 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||
|
||||
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
||||
- Paypal: [my paypalme page](https://www.paypal.com/paypalme/palm1r)
|
||||
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
||||
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
||||
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
||||
@@ -419,6 +515,11 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
||||
|
||||
Every contribution, no matter how small, is greatly appreciated and helps keep the project alive!
|
||||
|
||||
## Related Projects
|
||||
|
||||
- **[LLMQore](https://github.com/Palm1r/llmqore)** — the standalone LLM-core library extracted from QodeAssist, reusable in other Qt/C++ projects
|
||||
- **[QodeAssistUpdater](https://github.com/Palm1r/QodeAssistUpdater)** — CLI installer/updater for the plugin
|
||||
|
||||
## How to Build
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "RefactorSuggestion.hpp"
|
||||
#include "LLMSuggestion.hpp"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "RefactorSuggestionHoverHandler.hpp"
|
||||
#include "RefactorSuggestion.hpp"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https:
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "FlowEditor.hpp"
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https:
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "GridBackground.hpp"
|
||||
#include <QPainter>
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "BaseTask.hpp"
|
||||
#include "TaskPort.hpp"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "Flow.hpp"
|
||||
#include "TaskPort.hpp"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "FlowManager.hpp"
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "TaskConnection.hpp"
|
||||
#include "BaseTask.hpp"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "TaskPort.hpp"
|
||||
#include "TaskConnection.hpp"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "TaskRegistry.hpp"
|
||||
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -10,14 +10,16 @@ qt_add_qml_module(QodeAssistUIControls
|
||||
QML_FILES
|
||||
qml/Badge.qml
|
||||
qml/QoAButton.qml
|
||||
qml/QoABusyOverlay.qml
|
||||
qml/QoATextSlider.qml
|
||||
qml/QoAComboBox.qml
|
||||
qml/FadeListItemAnimation.qml
|
||||
qml/QoASeparator.qml
|
||||
qml/QoAToolTip.qml
|
||||
|
||||
RESOURCES
|
||||
icons/dropdown-arrow-light.svg
|
||||
icons/dropdown-arrow-dark.svg
|
||||
QML_FILES qml/QoASeparator.qml
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistUIControls
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
|
||||
|
||||
43
UIControls/qml/QoABusyOverlay.qml
Normal file
43
UIControls/qml/QoABusyOverlay.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
* QodeAssist is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* QodeAssist is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user