mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-13 17:59:15 -04:00
Compare commits
1 Commits
v0.9.20
...
add-qtc-19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
989063f6c8 |
2
.github/workflows/build_cmake.yml
vendored
2
.github/workflows/build_cmake.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
- {
|
- {
|
||||||
qt_version: "6.10.3",
|
qt_version: "6.10.3",
|
||||||
qt_creator_version: "19.0.2"
|
qt_creator_version: "19.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ cmake_minimum_required(VERSION 3.16)
|
|||||||
|
|
||||||
project(QodeAssist)
|
project(QodeAssist)
|
||||||
|
|
||||||
option(QODEASSIST_EXPERIMENTAL
|
|
||||||
"Enable experimental features" OFF)
|
|
||||||
message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}")
|
|
||||||
|
|
||||||
set(CMAKE_AUTOMOC ON)
|
set(CMAKE_AUTOMOC ON)
|
||||||
set(CMAKE_AUTORCC ON)
|
set(CMAKE_AUTORCC ON)
|
||||||
set(CMAKE_AUTOUIC ON)
|
set(CMAKE_AUTOUIC ON)
|
||||||
@@ -18,9 +14,7 @@ find_package(QtCreator REQUIRED COMPONENTS Core)
|
|||||||
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
|
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
|
||||||
find_package(GTest)
|
find_package(GTest)
|
||||||
|
|
||||||
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES
|
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
|
||||||
en cs zh_CN zh_TW da de fr hr ja pl ru sl sv uk
|
|
||||||
)
|
|
||||||
|
|
||||||
# IDE_VERSION is defined by QtCreator package
|
# IDE_VERSION is defined by QtCreator package
|
||||||
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
|
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
|
||||||
@@ -40,10 +34,10 @@ add_definitions(
|
|||||||
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(sources)
|
add_subdirectory(sources/external/llmqore)
|
||||||
add_subdirectory(logger)
|
|
||||||
add_subdirectory(pluginllmcore)
|
add_subdirectory(pluginllmcore)
|
||||||
add_subdirectory(settings)
|
add_subdirectory(settings)
|
||||||
|
add_subdirectory(logger)
|
||||||
add_subdirectory(UIControls)
|
add_subdirectory(UIControls)
|
||||||
add_subdirectory(ChatView)
|
add_subdirectory(ChatView)
|
||||||
add_subdirectory(context)
|
add_subdirectory(context)
|
||||||
@@ -70,9 +64,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
QtCreator::CPlusPlus
|
QtCreator::CPlusPlus
|
||||||
LLMQore
|
LLMQore
|
||||||
PluginLLMCore
|
PluginLLMCore
|
||||||
ProvidersConfig
|
|
||||||
Agents
|
|
||||||
Skills
|
|
||||||
QodeAssistChatViewplugin
|
QodeAssistChatViewplugin
|
||||||
SOURCES
|
SOURCES
|
||||||
.github/workflows/build_cmake.yml
|
.github/workflows/build_cmake.yml
|
||||||
@@ -90,6 +81,8 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/OpenAI.hpp
|
templates/OpenAI.hpp
|
||||||
templates/MistralAI.hpp
|
templates/MistralAI.hpp
|
||||||
templates/StarCoder2Fim.hpp
|
templates/StarCoder2Fim.hpp
|
||||||
|
# templates/DeepSeekCoderFim.hpp
|
||||||
|
# templates/CustomFimTemplate.hpp
|
||||||
templates/Qwen25CoderFIM.hpp
|
templates/Qwen25CoderFIM.hpp
|
||||||
templates/OpenAICompatible.hpp
|
templates/OpenAICompatible.hpp
|
||||||
templates/Llama3.hpp
|
templates/Llama3.hpp
|
||||||
@@ -102,7 +95,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/Qwen3CoderFIM.hpp
|
templates/Qwen3CoderFIM.hpp
|
||||||
templates/OpenAIResponses.hpp
|
templates/OpenAIResponses.hpp
|
||||||
providers/Providers.hpp
|
providers/Providers.hpp
|
||||||
providers/ProviderUrlUtils.hpp
|
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||||
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
||||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||||
@@ -115,10 +107,16 @@ add_qtc_plugin(QodeAssist
|
|||||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||||
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
||||||
|
providers/OpenAIResponses/ModelRequest.hpp
|
||||||
|
providers/OpenAIResponses/ResponseObject.hpp
|
||||||
|
providers/OpenAIResponses/GetResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/DeleteResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/CancelResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/ListInputItemsRequest.hpp
|
||||||
|
providers/OpenAIResponses/InputTokensRequest.hpp
|
||||||
|
providers/OpenAIResponses/ItemTypesReference.hpp
|
||||||
|
providers/OpenAIResponsesRequestBuilder.hpp
|
||||||
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
||||||
providers/QwenProvider.hpp providers/QwenProvider.cpp
|
|
||||||
providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp
|
|
||||||
providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp
|
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
LLMSuggestion.hpp LLMSuggestion.cpp
|
LLMSuggestion.hpp LLMSuggestion.cpp
|
||||||
@@ -127,9 +125,6 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssistClient.hpp QodeAssistClient.cpp
|
QodeAssistClient.hpp QodeAssistClient.cpp
|
||||||
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
|
||||||
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
|
||||||
chat/ChatDocument.hpp chat/ChatDocument.cpp
|
|
||||||
chat/ChatEditor.hpp chat/ChatEditor.cpp
|
|
||||||
chat/ChatEditorFactory.hpp chat/ChatEditorFactory.cpp
|
|
||||||
ConfigurationManager.hpp ConfigurationManager.cpp
|
ConfigurationManager.hpp ConfigurationManager.cpp
|
||||||
CodeHandler.hpp CodeHandler.cpp
|
CodeHandler.hpp CodeHandler.cpp
|
||||||
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
|
||||||
@@ -162,19 +157,12 @@ add_qtc_plugin(QodeAssist
|
|||||||
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
|
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
|
||||||
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||||
tools/TodoTool.hpp tools/TodoTool.cpp
|
tools/TodoTool.hpp tools/TodoTool.cpp
|
||||||
tools/ReadOriginalHistoryTool.hpp tools/ReadOriginalHistoryTool.cpp
|
|
||||||
tools/SkillTool.hpp tools/SkillTool.cpp
|
|
||||||
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
|
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
|
||||||
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
|
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
|
||||||
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
|
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
|
||||||
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if(QODEASSIST_EXPERIMENTAL)
|
|
||||||
target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL)
|
|
||||||
target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
|
||||||
find_program(QtCreatorExecutable
|
find_program(QtCreatorExecutable
|
||||||
NAMES
|
NAMES
|
||||||
@@ -196,5 +184,5 @@ endif()
|
|||||||
qt_add_translations(TARGETS QodeAssist
|
qt_add_translations(TARGETS QodeAssist
|
||||||
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
|
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
|
||||||
RESOURCE_PREFIX "/translations"
|
RESOURCE_PREFIX "/translations"
|
||||||
LUPDATE_OPTIONS -no-obsolete -locations none
|
LUPDATE_OPTIONS -no-obsolete
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "AgentRoleController.hpp"
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "AgentRole.hpp"
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
AgentRoleController::AgentRoleController(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().systemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&AgentRoleController::baseSystemPromptChanged);
|
|
||||||
|
|
||||||
loadAvailableRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList AgentRoleController::availableRoles() const
|
|
||||||
{
|
|
||||||
return m_availableRoles;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRole() const
|
|
||||||
{
|
|
||||||
return m_currentRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::baseSystemPrompt() const
|
|
||||||
{
|
|
||||||
return Settings::chatAssistantSettings().systemPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRoleDescription() const
|
|
||||||
{
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
if (lastRoleId.isEmpty())
|
|
||||||
return Settings::AgentRolesManager::getNoRole().description;
|
|
||||||
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (role.id.isEmpty())
|
|
||||||
return Settings::AgentRolesManager::getNoRole().description;
|
|
||||||
|
|
||||||
return role.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString AgentRoleController::currentRoleSystemPrompt() const
|
|
||||||
{
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
if (lastRoleId.isEmpty())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
|
||||||
if (role.id.isEmpty())
|
|
||||||
return QString();
|
|
||||||
|
|
||||||
return role.systemPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::loadAvailableRoles()
|
|
||||||
{
|
|
||||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
|
||||||
|
|
||||||
m_availableRoles.clear();
|
|
||||||
m_availableRoles.append(Settings::AgentRolesManager::getNoRole().name);
|
|
||||||
|
|
||||||
for (const auto &role : roles)
|
|
||||||
m_availableRoles.append(role.name);
|
|
||||||
|
|
||||||
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
|
|
||||||
m_currentRole = Settings::AgentRolesManager::getNoRole().name;
|
|
||||||
|
|
||||||
if (!lastRoleId.isEmpty()) {
|
|
||||||
for (const auto &role : roles) {
|
|
||||||
if (role.id == lastRoleId) {
|
|
||||||
m_currentRole = role.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emit availableRolesChanged();
|
|
||||||
emit currentRoleChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::applyRole(const QString &roleName)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
|
||||||
|
|
||||||
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
|
|
||||||
settings.lastUsedRoleId.setValue("");
|
|
||||||
settings.writeSettings();
|
|
||||||
m_currentRole = roleName;
|
|
||||||
emit currentRoleChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
|
|
||||||
|
|
||||||
for (const auto &role : roles) {
|
|
||||||
if (role.name == roleName) {
|
|
||||||
settings.lastUsedRoleId.setValue(role.id);
|
|
||||||
settings.writeSettings();
|
|
||||||
m_currentRole = role.name;
|
|
||||||
emit currentRoleChanged();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AgentRoleController::openSettings()
|
|
||||||
{
|
|
||||||
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class AgentRoleController : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit AgentRoleController(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QStringList availableRoles() const;
|
|
||||||
QString currentRole() const;
|
|
||||||
QString baseSystemPrompt() const;
|
|
||||||
QString currentRoleDescription() const;
|
|
||||||
QString currentRoleSystemPrompt() const;
|
|
||||||
|
|
||||||
void loadAvailableRoles();
|
|
||||||
void applyRole(const QString &roleName);
|
|
||||||
void openSettings();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void availableRolesChanged();
|
|
||||||
void currentRoleChanged();
|
|
||||||
void baseSystemPromptChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QStringList m_availableRoles;
|
|
||||||
QString m_currentRole;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -23,11 +23,9 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
qml/controls/FileMentionPopup.qml
|
qml/controls/FileMentionPopup.qml
|
||||||
qml/controls/FileEditsActionBar.qml
|
qml/controls/FileEditsActionBar.qml
|
||||||
qml/controls/ContextViewer.qml
|
qml/controls/ContextViewer.qml
|
||||||
qml/controls/SkillCommandPopup.qml
|
|
||||||
qml/controls/Toast.qml
|
qml/controls/Toast.qml
|
||||||
qml/controls/TopBar.qml
|
qml/controls/TopBar.qml
|
||||||
qml/controls/SplitDropZone.qml
|
qml/controls/SplitDropZone.qml
|
||||||
qml/controls/MessageNavigator.qml
|
|
||||||
|
|
||||||
RESOURCES
|
RESOURCES
|
||||||
icons/attach-file-light.svg
|
icons/attach-file-light.svg
|
||||||
@@ -45,12 +43,9 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/window-unlock.svg
|
icons/window-unlock.svg
|
||||||
icons/chat-icon.svg
|
icons/chat-icon.svg
|
||||||
icons/chat-pause-icon.svg
|
icons/chat-pause-icon.svg
|
||||||
icons/warning-icon.svg
|
|
||||||
icons/new-chat-icon.svg
|
|
||||||
icons/rules-icon.svg
|
icons/rules-icon.svg
|
||||||
icons/context-icon.svg
|
icons/context-icon.svg
|
||||||
icons/open-in-editor.svg
|
icons/open-in-editor.svg
|
||||||
icons/open-in-window.svg
|
|
||||||
icons/apply-changes-button.svg
|
icons/apply-changes-button.svg
|
||||||
icons/undo-changes-button.svg
|
icons/undo-changes-button.svg
|
||||||
icons/reject-changes-button.svg
|
icons/reject-changes-button.svg
|
||||||
@@ -60,7 +55,6 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/tools-icon-off.svg
|
icons/tools-icon-off.svg
|
||||||
icons/settings-icon.svg
|
icons/settings-icon.svg
|
||||||
icons/compress-icon.svg
|
icons/compress-icon.svg
|
||||||
icons/open-in-code.svg
|
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
@@ -75,13 +69,7 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
FileItem.hpp FileItem.cpp
|
FileItem.hpp FileItem.cpp
|
||||||
ChatFileManager.hpp ChatFileManager.cpp
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
ChatCompressor.hpp ChatCompressor.cpp
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
AgentRoleController.hpp AgentRoleController.cpp
|
|
||||||
ChatConfigurationController.hpp ChatConfigurationController.cpp
|
|
||||||
FileEditController.hpp FileEditController.cpp
|
|
||||||
InputTokenCounter.hpp InputTokenCounter.cpp
|
|
||||||
ChatHistoryStore.hpp ChatHistoryStore.cpp
|
|
||||||
FileMentionItem.hpp FileMentionItem.cpp
|
FileMentionItem.hpp FileMentionItem.cpp
|
||||||
SessionFileRegistry.hpp SessionFileRegistry.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
@@ -98,9 +86,8 @@ target_link_libraries(QodeAssistChatView
|
|||||||
QodeAssistUIControlsplugin
|
QodeAssistUIControlsplugin
|
||||||
QodeAssistLogger
|
QodeAssistLogger
|
||||||
LLMQore
|
LLMQore
|
||||||
Skills
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}
|
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatCompressor.hpp"
|
#include "ChatCompressor.hpp"
|
||||||
|
|
||||||
@@ -76,8 +75,6 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch
|
|||||||
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||||
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||||
: promptTemplate->endpoint();
|
: promptTemplate->endpoint();
|
||||||
m_provider->client()->setTransferTimeout(
|
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
|
||||||
m_currentRequestId = m_provider->sendRequest(
|
m_currentRequestId = m_provider->sendRequest(
|
||||||
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||||
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
@@ -231,8 +228,6 @@ bool ChatCompressor::createCompressedChatFile(
|
|||||||
summaryMessage["images"] = QJsonArray();
|
summaryMessage["images"] = QJsonArray();
|
||||||
|
|
||||||
root["messages"] = QJsonArray{summaryMessage};
|
root["messages"] = QJsonArray{summaryMessage};
|
||||||
root["compressedFrom"] = sourcePath;
|
|
||||||
root["compressedAt"] = QDateTime::currentDateTime().toString(Qt::ISODate);
|
|
||||||
|
|
||||||
if (QFile::exists(destPath))
|
if (QFile::exists(destPath))
|
||||||
QFile::remove(destPath);
|
QFile::remove(destPath);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatConfigurationController.hpp"
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
ChatConfigurationController::ChatConfigurationController(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
connect(
|
|
||||||
&settings.caProvider,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatConfigurationController::updateCurrentConfiguration);
|
|
||||||
connect(
|
|
||||||
&settings.caModel,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatConfigurationController::updateCurrentConfiguration);
|
|
||||||
|
|
||||||
loadAvailableConfigurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList ChatConfigurationController::availableConfigurations() const
|
|
||||||
{
|
|
||||||
return m_availableConfigurations;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatConfigurationController::currentConfiguration() const
|
|
||||||
{
|
|
||||||
return m_currentConfiguration;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::updateCurrentConfiguration()
|
|
||||||
{
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
m_currentConfiguration
|
|
||||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::loadAvailableConfigurations()
|
|
||||||
{
|
|
||||||
auto &manager = Settings::ConfigurationManager::instance();
|
|
||||||
manager.loadConfigurations(Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
|
||||||
Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
m_availableConfigurations.clear();
|
|
||||||
m_availableConfigurations.append(QObject::tr("Current Settings"));
|
|
||||||
|
|
||||||
for (const Settings::AIConfiguration &config : configs) {
|
|
||||||
m_availableConfigurations.append(config.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentConfiguration();
|
|
||||||
|
|
||||||
emit availableConfigurationsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatConfigurationController::applyConfiguration(const QString &configName)
|
|
||||||
{
|
|
||||||
if (configName == QObject::tr("Current Settings")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &manager = Settings::ConfigurationManager::instance();
|
|
||||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
|
||||||
Settings::ConfigurationType::Chat);
|
|
||||||
|
|
||||||
for (const Settings::AIConfiguration &config : configs) {
|
|
||||||
if (config.name == configName) {
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
|
|
||||||
settings.caProvider.setValue(config.provider);
|
|
||||||
settings.caModel.setValue(config.model);
|
|
||||||
settings.caTemplate.setValue(config.templateName);
|
|
||||||
settings.caUrl.setValue(config.url);
|
|
||||||
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
|
||||||
|
|
||||||
settings.writeSettings();
|
|
||||||
|
|
||||||
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class ChatConfigurationController : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ChatConfigurationController(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QStringList availableConfigurations() const;
|
|
||||||
QString currentConfiguration() const;
|
|
||||||
|
|
||||||
void loadAvailableConfigurations();
|
|
||||||
void applyConfiguration(const QString &configName);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void availableConfigurationsChanged();
|
|
||||||
void currentConfigurationChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateCurrentConfiguration();
|
|
||||||
|
|
||||||
QStringList m_availableConfigurations;
|
|
||||||
QString m_currentConfiguration;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatFileManager.hpp"
|
#include "ChatFileManager.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatHistoryStore.hpp"
|
|
||||||
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QDesktopServices>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QUrl>
|
|
||||||
|
|
||||||
#include <coreplugin/icore.h>
|
|
||||||
#include <projectexplorer/project.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "Logger.hpp"
|
|
||||||
#include "ProjectSettings.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_chatModel(chatModel)
|
|
||||||
{}
|
|
||||||
|
|
||||||
QString ChatHistoryStore::historyDir() const
|
|
||||||
{
|
|
||||||
QString path;
|
|
||||||
|
|
||||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
|
||||||
Settings::ProjectSettings projectSettings(project);
|
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
|
||||||
} else {
|
|
||||||
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
|
||||||
path = baseDir.filePath("qodeassist/chat_history");
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir dir(path);
|
|
||||||
if (!dir.exists() && !dir.mkpath(".")) {
|
|
||||||
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatHistoryStore::suggestedFileName() const
|
|
||||||
{
|
|
||||||
QString shortMessage;
|
|
||||||
|
|
||||||
if (m_chatModel->rowCount() > 0) {
|
|
||||||
QString firstMessage
|
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
|
||||||
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
|
||||||
|
|
||||||
if (shortMessage.isEmpty()) {
|
|
||||||
QVariantList images
|
|
||||||
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
|
||||||
if (!images.isEmpty()) {
|
|
||||||
shortMessage = "image_chat";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return generateChatFileName(shortMessage, historyDir());
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatHistoryStore::autosaveFilePath(const QString &recentFilePath) const
|
|
||||||
{
|
|
||||||
if (!recentFilePath.isEmpty()) {
|
|
||||||
return recentFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString dir = historyDir();
|
|
||||||
if (dir.isEmpty()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return QDir(dir).filePath(suggestedFileName() + ".json");
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatHistoryStore::autosaveFilePath(
|
|
||||||
const QString &recentFilePath, const QString &firstMessage, bool hasImageAttachments) const
|
|
||||||
{
|
|
||||||
if (!recentFilePath.isEmpty()) {
|
|
||||||
return recentFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString dir = historyDir();
|
|
||||||
if (dir.isEmpty()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
|
||||||
|
|
||||||
if (shortMessage.isEmpty() && hasImageAttachments) {
|
|
||||||
shortMessage = "image_chat";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString fileName = generateChatFileName(shortMessage, dir);
|
|
||||||
return QDir(dir).filePath(fileName + ".json");
|
|
||||||
}
|
|
||||||
|
|
||||||
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
|
||||||
{
|
|
||||||
return ChatSerializer::saveToFile(m_chatModel, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
|
||||||
{
|
|
||||||
return ChatSerializer::loadFromFile(m_chatModel, filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatHistoryStore::showSaveDialog()
|
|
||||||
{
|
|
||||||
QString initialDir = historyDir();
|
|
||||||
|
|
||||||
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
|
|
||||||
dialog->setAcceptMode(QFileDialog::AcceptSave);
|
|
||||||
dialog->setFileMode(QFileDialog::AnyFile);
|
|
||||||
dialog->setNameFilter(tr("JSON files (*.json)"));
|
|
||||||
dialog->setDefaultSuffix("json");
|
|
||||||
if (!initialDir.isEmpty()) {
|
|
||||||
dialog->setDirectory(initialDir);
|
|
||||||
dialog->selectFile(suggestedFileName() + ".json");
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
|
||||||
if (result == QFileDialog::Accepted) {
|
|
||||||
QStringList files = dialog->selectedFiles();
|
|
||||||
if (!files.isEmpty()) {
|
|
||||||
emit saveRequested(files.first());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialog->deleteLater();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog->open();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatHistoryStore::showLoadDialog()
|
|
||||||
{
|
|
||||||
QString initialDir = historyDir();
|
|
||||||
|
|
||||||
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
|
|
||||||
dialog->setAcceptMode(QFileDialog::AcceptOpen);
|
|
||||||
dialog->setFileMode(QFileDialog::ExistingFile);
|
|
||||||
dialog->setNameFilter(tr("JSON files (*.json)"));
|
|
||||||
if (!initialDir.isEmpty()) {
|
|
||||||
dialog->setDirectory(initialDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
|
||||||
if (result == QFileDialog::Accepted) {
|
|
||||||
QStringList files = dialog->selectedFiles();
|
|
||||||
if (!files.isEmpty()) {
|
|
||||||
emit loadRequested(files.first());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialog->deleteLater();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog->open();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatHistoryStore::openHistoryFolder() const
|
|
||||||
{
|
|
||||||
QString path;
|
|
||||||
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
|
||||||
Settings::ProjectSettings projectSettings(project);
|
|
||||||
path = projectSettings.chatHistoryPath().toFSPathString();
|
|
||||||
} else {
|
|
||||||
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
|
||||||
path = baseDir.filePath("qodeassist/chat_history");
|
|
||||||
}
|
|
||||||
|
|
||||||
QDir dir(path);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
dir.mkpath(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
|
||||||
QDesktopServices::openUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatHistoryStore::generateChatFileName(const QString &shortMessage, const QString &dir) const
|
|
||||||
{
|
|
||||||
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
|
|
||||||
static const QRegularExpression underSymbols = QRegularExpression("_+");
|
|
||||||
|
|
||||||
QStringList parts;
|
|
||||||
QString sanitizedMessage = shortMessage;
|
|
||||||
sanitizedMessage.replace(saitizeSymbols, "_");
|
|
||||||
sanitizedMessage.replace(underSymbols, "_");
|
|
||||||
sanitizedMessage = sanitizedMessage.trimmed();
|
|
||||||
|
|
||||||
if (!sanitizedMessage.isEmpty()) {
|
|
||||||
if (sanitizedMessage.startsWith('_')) {
|
|
||||||
sanitizedMessage.remove(0, 1);
|
|
||||||
}
|
|
||||||
if (sanitizedMessage.endsWith('_')) {
|
|
||||||
sanitizedMessage.chop(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString fullPath = QDir(dir).filePath(sanitizedMessage);
|
|
||||||
QFileInfo fileInfo(fullPath);
|
|
||||||
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
|
|
||||||
parts << sanitizedMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
|
|
||||||
|
|
||||||
QString fileName = parts.join("_");
|
|
||||||
QString fullPath = QDir(dir).filePath(fileName);
|
|
||||||
QFileInfo finalCheck(fullPath);
|
|
||||||
|
|
||||||
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
|
||||||
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
#include "ChatSerializer.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class ChatHistoryStore : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
QString historyDir() const;
|
|
||||||
QString suggestedFileName() const;
|
|
||||||
QString autosaveFilePath(const QString &recentFilePath) const;
|
|
||||||
QString autosaveFilePath(
|
|
||||||
const QString &recentFilePath,
|
|
||||||
const QString &firstMessage,
|
|
||||||
bool hasImageAttachments) const;
|
|
||||||
|
|
||||||
SerializationResult save(const QString &filePath) const;
|
|
||||||
SerializationResult load(const QString &filePath) const;
|
|
||||||
|
|
||||||
void showSaveDialog();
|
|
||||||
void showLoadDialog();
|
|
||||||
void openHistoryFolder() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void saveRequested(const QString &filePath);
|
|
||||||
void loadRequested(const QString &filePath);
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include <utils/aspects.h>
|
#include <utils/aspects.h>
|
||||||
@@ -12,6 +11,7 @@
|
|||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QtQml>
|
#include <QtQml>
|
||||||
|
|
||||||
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "context/ChangesManager.h"
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
@@ -20,6 +20,14 @@ namespace QodeAssist::Chat {
|
|||||||
ChatModel::ChatModel(QObject *parent)
|
ChatModel::ChatModel(QObject *parent)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
{
|
{
|
||||||
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
|
connect(
|
||||||
|
&settings.chatTokensThreshold,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatModel::tokensThresholdChanged);
|
||||||
|
|
||||||
connect(&Context::ChangesManager::instance(),
|
connect(&Context::ChangesManager::instance(),
|
||||||
&Context::ChangesManager::fileEditApplied,
|
&Context::ChangesManager::fileEditApplied,
|
||||||
this,
|
this,
|
||||||
@@ -78,16 +86,6 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
case Roles::IsRedacted: {
|
case Roles::IsRedacted: {
|
||||||
return message.isRedacted;
|
return message.isRedacted;
|
||||||
}
|
}
|
||||||
case Roles::PromptTokens:
|
|
||||||
return message.promptTokens;
|
|
||||||
case Roles::CompletionTokens:
|
|
||||||
return message.completionTokens;
|
|
||||||
case Roles::CachedPromptTokens:
|
|
||||||
return message.cachedPromptTokens;
|
|
||||||
case Roles::ReasoningTokens:
|
|
||||||
return message.reasoningTokens;
|
|
||||||
case Roles::TotalTokens:
|
|
||||||
return message.promptTokens + message.completionTokens;
|
|
||||||
case Roles::Images: {
|
case Roles::Images: {
|
||||||
QVariantList imagesList;
|
QVariantList imagesList;
|
||||||
for (const auto &image : message.images) {
|
for (const auto &image : message.images) {
|
||||||
@@ -126,11 +124,6 @@ QHash<int, QByteArray> ChatModel::roleNames() const
|
|||||||
roles[Roles::Attachments] = "attachments";
|
roles[Roles::Attachments] = "attachments";
|
||||||
roles[Roles::IsRedacted] = "isRedacted";
|
roles[Roles::IsRedacted] = "isRedacted";
|
||||||
roles[Roles::Images] = "images";
|
roles[Roles::Images] = "images";
|
||||||
roles[Roles::PromptTokens] = "promptTokens";
|
|
||||||
roles[Roles::CompletionTokens] = "completionTokens";
|
|
||||||
roles[Roles::CachedPromptTokens] = "cachedPromptTokens";
|
|
||||||
roles[Roles::ReasoningTokens] = "reasoningTokens";
|
|
||||||
roles[Roles::TotalTokens] = "totalTokens";
|
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +207,6 @@ void ChatModel::clear()
|
|||||||
m_messages.clear();
|
m_messages.clear();
|
||||||
endResetModel();
|
endResetModel();
|
||||||
emit modelReseted();
|
emit modelReseted();
|
||||||
emit sessionUsageChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
||||||
@@ -318,6 +310,12 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
|
|||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ChatModel::tokensThreshold() const
|
||||||
|
{
|
||||||
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
return settings.chatTokensThreshold();
|
||||||
|
}
|
||||||
|
|
||||||
QString ChatModel::lastMessageId() const
|
QString ChatModel::lastMessageId() const
|
||||||
{
|
{
|
||||||
return !m_messages.isEmpty() ? m_messages.last().id : "";
|
return !m_messages.isEmpty() ? m_messages.last().id : "";
|
||||||
@@ -332,37 +330,11 @@ void ChatModel::resetModelTo(int index)
|
|||||||
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
|
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
|
||||||
m_messages.remove(index, m_messages.size() - index);
|
m_messages.remove(index, m_messages.size() - index);
|
||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
emit sessionUsageChanged();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList ChatModel::userMessagePreviews(int maxLength) const
|
|
||||||
{
|
|
||||||
QVariantList result;
|
|
||||||
const int limit = maxLength > 4 ? maxLength : 80;
|
|
||||||
for (int i = 0; i < m_messages.size(); ++i) {
|
|
||||||
if (m_messages[i].role != ChatRole::User)
|
|
||||||
continue;
|
|
||||||
QString preview = m_messages[i].content;
|
|
||||||
preview.replace(QLatin1Char('\n'), QLatin1Char(' '));
|
|
||||||
preview.replace(QLatin1Char('\r'), QLatin1Char(' '));
|
|
||||||
preview.replace(QLatin1Char('\t'), QLatin1Char(' '));
|
|
||||||
preview = preview.simplified();
|
|
||||||
if (preview.size() > limit)
|
|
||||||
preview = preview.left(limit - 1).trimmed() + QChar(0x2026);
|
|
||||||
QVariantMap entry;
|
|
||||||
entry[QStringLiteral("messageIndex")] = i;
|
|
||||||
entry[QStringLiteral("preview")] = preview;
|
|
||||||
result.append(entry);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::addToolExecutionStatus(
|
void ChatModel::addToolExecutionStatus(
|
||||||
const QString &requestId,
|
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments)
|
|
||||||
{
|
{
|
||||||
QString content = toolName;
|
QString content = toolName;
|
||||||
|
|
||||||
@@ -373,15 +345,11 @@ void ChatModel::addToolExecutionStatus(
|
|||||||
&& m_messages.last().role == ChatRole::Tool) {
|
&& m_messages.last().role == ChatRole::Tool) {
|
||||||
Message &lastMessage = m_messages.last();
|
Message &lastMessage = m_messages.last();
|
||||||
lastMessage.content = content;
|
lastMessage.content = content;
|
||||||
lastMessage.toolName = toolName;
|
|
||||||
lastMessage.toolArguments = toolArguments;
|
|
||||||
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
|
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
|
||||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||||
} else {
|
} else {
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||||
Message newMessage{ChatRole::Tool, content, toolId};
|
Message newMessage{ChatRole::Tool, content, toolId};
|
||||||
newMessage.toolName = toolName;
|
|
||||||
newMessage.toolArguments = toolArguments;
|
|
||||||
m_messages.append(newMessage);
|
m_messages.append(newMessage);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
|
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
|
||||||
@@ -390,38 +358,6 @@ void ChatModel::addToolExecutionStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatModel::dropTrailingAssistantMessage(const QString &requestId)
|
|
||||||
{
|
|
||||||
if (m_messages.isEmpty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const Message &last = m_messages.last();
|
|
||||||
if (last.role != ChatRole::Assistant || last.id != requestId)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const int idx = m_messages.size() - 1;
|
|
||||||
beginRemoveRows(QModelIndex(), idx, idx);
|
|
||||||
m_messages.removeLast();
|
|
||||||
endRemoveRows();
|
|
||||||
LOG_MESSAGE(QString("Dropped leaked pre-tool assistant message at index %1").arg(idx));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::setToolMessageData(
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments,
|
|
||||||
const QString &toolResult)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < m_messages.size(); ++i) {
|
|
||||||
if (m_messages[i].role == ChatRole::Tool && m_messages[i].id == toolId) {
|
|
||||||
m_messages[i].toolName = toolName;
|
|
||||||
m_messages[i].toolArguments = toolArguments;
|
|
||||||
m_messages[i].toolResult = toolResult;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::updateToolResult(
|
void ChatModel::updateToolResult(
|
||||||
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
|
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
|
||||||
{
|
{
|
||||||
@@ -441,8 +377,6 @@ void ChatModel::updateToolResult(
|
|||||||
for (int i = m_messages.size() - 1; i >= 0; --i) {
|
for (int i = m_messages.size() - 1; i >= 0; --i) {
|
||||||
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
|
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
|
||||||
m_messages[i].content = toolName + "\n" + result;
|
m_messages[i].content = toolName + "\n" + result;
|
||||||
m_messages[i].toolName = toolName;
|
|
||||||
m_messages[i].toolResult = result;
|
|
||||||
emit dataChanged(index(i), index(i));
|
emit dataChanged(index(i), index(i));
|
||||||
toolMessageFound = true;
|
toolMessageFound = true;
|
||||||
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
|
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
|
||||||
@@ -573,62 +507,6 @@ void ChatModel::updateMessageContent(const QString &messageId, const QString &ne
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatModel::setMessageUsage(
|
|
||||||
const QString &messageId,
|
|
||||||
int promptTokens,
|
|
||||||
int completionTokens,
|
|
||||||
int cachedPromptTokens,
|
|
||||||
int reasoningTokens)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < m_messages.size(); ++i) {
|
|
||||||
if (m_messages[i].id != messageId)
|
|
||||||
continue;
|
|
||||||
m_messages[i].promptTokens = promptTokens;
|
|
||||||
m_messages[i].completionTokens = completionTokens;
|
|
||||||
m_messages[i].cachedPromptTokens = cachedPromptTokens;
|
|
||||||
m_messages[i].reasoningTokens = reasoningTokens;
|
|
||||||
emit dataChanged(
|
|
||||||
index(i),
|
|
||||||
index(i),
|
|
||||||
{Roles::PromptTokens,
|
|
||||||
Roles::CompletionTokens,
|
|
||||||
Roles::CachedPromptTokens,
|
|
||||||
Roles::ReasoningTokens,
|
|
||||||
Roles::TotalTokens});
|
|
||||||
emit sessionUsageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatModel::sessionPromptTokens() const
|
|
||||||
{
|
|
||||||
int total = 0;
|
|
||||||
for (const auto &m : m_messages)
|
|
||||||
total += m.promptTokens;
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatModel::sessionCompletionTokens() const
|
|
||||||
{
|
|
||||||
int total = 0;
|
|
||||||
for (const auto &m : m_messages)
|
|
||||||
total += m.completionTokens;
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatModel::sessionCachedPromptTokens() const
|
|
||||||
{
|
|
||||||
int total = 0;
|
|
||||||
for (const auto &m : m_messages)
|
|
||||||
total += m.cachedPromptTokens;
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ChatModel::sessionTotalTokens() const
|
|
||||||
{
|
|
||||||
return sessionPromptTokens() + sessionCompletionTokens();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatModel::setLoadingFromHistory(bool loading)
|
void ChatModel::setLoadingFromHistory(bool loading)
|
||||||
{
|
{
|
||||||
m_loadingFromHistory = loading;
|
m_loadingFromHistory = loading;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
|
||||||
#include <QtQmlIntegration>
|
#include <QtQmlIntegration>
|
||||||
|
|
||||||
#include "context/ContentFile.hpp"
|
#include "context/ContentFile.hpp"
|
||||||
@@ -19,28 +17,14 @@ namespace QodeAssist::Chat {
|
|||||||
class ChatModel : public QAbstractListModel
|
class ChatModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(int sessionPromptTokens READ sessionPromptTokens NOTIFY sessionUsageChanged FINAL)
|
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
|
||||||
Q_PROPERTY(int sessionCompletionTokens READ sessionCompletionTokens NOTIFY sessionUsageChanged FINAL)
|
|
||||||
Q_PROPERTY(int sessionCachedPromptTokens READ sessionCachedPromptTokens NOTIFY sessionUsageChanged FINAL)
|
|
||||||
Q_PROPERTY(int sessionTotalTokens READ sessionTotalTokens NOTIFY sessionUsageChanged FINAL)
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||||
Q_ENUM(ChatRole)
|
Q_ENUM(ChatRole)
|
||||||
|
|
||||||
enum Roles {
|
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted, Images };
|
||||||
RoleType = Qt::UserRole,
|
|
||||||
Content,
|
|
||||||
Attachments,
|
|
||||||
IsRedacted,
|
|
||||||
Images,
|
|
||||||
PromptTokens,
|
|
||||||
CompletionTokens,
|
|
||||||
CachedPromptTokens,
|
|
||||||
ReasoningTokens,
|
|
||||||
TotalTokens
|
|
||||||
};
|
|
||||||
Q_ENUM(Roles)
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
struct ImageAttachment
|
struct ImageAttachment
|
||||||
@@ -60,15 +44,6 @@ public:
|
|||||||
|
|
||||||
QList<Context::ContentFile> attachments;
|
QList<Context::ContentFile> attachments;
|
||||||
QList<ImageAttachment> images;
|
QList<ImageAttachment> images;
|
||||||
|
|
||||||
QString toolName;
|
|
||||||
QJsonObject toolArguments;
|
|
||||||
QString toolResult;
|
|
||||||
|
|
||||||
int promptTokens = 0;
|
|
||||||
int completionTokens = 0;
|
|
||||||
int cachedPromptTokens = 0;
|
|
||||||
int reasoningTokens = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit ChatModel(QObject *parent = nullptr);
|
explicit ChatModel(QObject *parent = nullptr);
|
||||||
@@ -91,23 +66,15 @@ public:
|
|||||||
QVector<Message> getChatHistory() const;
|
QVector<Message> getChatHistory() const;
|
||||||
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
|
||||||
|
|
||||||
|
int tokensThreshold() const;
|
||||||
|
|
||||||
QString currentModel() const;
|
QString currentModel() const;
|
||||||
QString lastMessageId() const;
|
QString lastMessageId() const;
|
||||||
|
|
||||||
Q_INVOKABLE void resetModelTo(int index);
|
Q_INVOKABLE void resetModelTo(int index);
|
||||||
Q_INVOKABLE QVariantList userMessagePreviews(int maxLength = 80) const;
|
|
||||||
|
|
||||||
void addToolExecutionStatus(
|
void addToolExecutionStatus(
|
||||||
const QString &requestId,
|
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments);
|
|
||||||
void dropTrailingAssistantMessage(const QString &requestId);
|
|
||||||
void setToolMessageData(
|
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &toolArguments,
|
|
||||||
const QString &toolResult);
|
|
||||||
void updateToolResult(
|
void updateToolResult(
|
||||||
const QString &requestId,
|
const QString &requestId,
|
||||||
const QString &toolId,
|
const QString &toolId,
|
||||||
@@ -118,18 +85,6 @@ public:
|
|||||||
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
|
||||||
void updateMessageContent(const QString &messageId, const QString &newContent);
|
void updateMessageContent(const QString &messageId, const QString &newContent);
|
||||||
|
|
||||||
void setMessageUsage(
|
|
||||||
const QString &messageId,
|
|
||||||
int promptTokens,
|
|
||||||
int completionTokens,
|
|
||||||
int cachedPromptTokens,
|
|
||||||
int reasoningTokens);
|
|
||||||
|
|
||||||
int sessionPromptTokens() const;
|
|
||||||
int sessionCompletionTokens() const;
|
|
||||||
int sessionCachedPromptTokens() const;
|
|
||||||
int sessionTotalTokens() const;
|
|
||||||
|
|
||||||
void setLoadingFromHistory(bool loading);
|
void setLoadingFromHistory(bool loading);
|
||||||
bool isLoadingFromHistory() const;
|
bool isLoadingFromHistory() const;
|
||||||
|
|
||||||
@@ -137,8 +92,8 @@ public:
|
|||||||
QString chatFilePath() const;
|
QString chatFilePath() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
void tokensThresholdChanged();
|
||||||
void modelReseted();
|
void modelReseted();
|
||||||
void sessionUsageChanged();
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onFileEditApplied(const QString &editId);
|
void onFileEditApplied(const QString &editId);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,8 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QPointer>
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
#include <QVariantList>
|
#include <QVariantList>
|
||||||
|
|
||||||
@@ -14,19 +12,9 @@
|
|||||||
#include "pluginllmcore/PromptProviderChat.hpp"
|
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
|
||||||
class SkillsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ChatCompressor;
|
class ChatCompressor;
|
||||||
class AgentRoleController;
|
|
||||||
class ChatConfigurationController;
|
|
||||||
class FileEditController;
|
|
||||||
class InputTokenCounter;
|
|
||||||
class ChatHistoryStore;
|
|
||||||
class SessionFileRegistry;
|
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
class ChatRootView : public QQuickItem
|
||||||
{
|
{
|
||||||
@@ -50,7 +38,6 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||||
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
||||||
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
|
||||||
Q_PROPERTY(QString sendShortcutText READ sendShortcutText NOTIFY sendShortcutTextChanged FINAL)
|
|
||||||
|
|
||||||
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
@@ -65,14 +52,11 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||||
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||||
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL)
|
|
||||||
Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL)
|
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ChatRootView(QQuickItem *parent = nullptr);
|
ChatRootView(QQuickItem *parent = nullptr);
|
||||||
~ChatRootView() override;
|
|
||||||
|
|
||||||
ChatModel *chatModel() const;
|
ChatModel *chatModel() const;
|
||||||
QString currentTemplate() const;
|
QString currentTemplate() const;
|
||||||
@@ -100,8 +84,6 @@ public:
|
|||||||
Q_INVOKABLE void showAddImageDialog();
|
Q_INVOKABLE void showAddImageDialog();
|
||||||
Q_INVOKABLE bool isImageFile(const QString &filePath) const;
|
Q_INVOKABLE bool isImageFile(const QString &filePath) const;
|
||||||
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
|
||||||
Q_INVOKABLE bool isSendShortcut(int key, int modifiers) const;
|
|
||||||
QString sendShortcutText() const;
|
|
||||||
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
|
||||||
Q_INVOKABLE void openChatHistoryFolder();
|
Q_INVOKABLE void openChatHistoryFolder();
|
||||||
Q_INVOKABLE void openRulesFolder();
|
Q_INVOKABLE void openRulesFolder();
|
||||||
@@ -109,11 +91,6 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
|
|
||||||
Q_INVOKABLE void relocateToSplit();
|
|
||||||
Q_INVOKABLE void relocateToWindow();
|
|
||||||
|
|
||||||
void consumePendingChatFile();
|
|
||||||
|
|
||||||
Q_INVOKABLE void updateInputTokensCount();
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
|
|
||||||
@@ -145,8 +122,6 @@ public:
|
|||||||
Q_INVOKABLE QString getRuleContent(int index);
|
Q_INVOKABLE QString getRuleContent(int index);
|
||||||
Q_INVOKABLE void refreshRules();
|
Q_INVOKABLE void refreshRules();
|
||||||
|
|
||||||
Q_INVOKABLE QVariantList searchSkills(const QString &query) const;
|
|
||||||
|
|
||||||
bool useTools() const;
|
bool useTools() const;
|
||||||
void setUseTools(bool enabled);
|
void setUseTools(bool enabled);
|
||||||
bool useThinking() const;
|
bool useThinking() const;
|
||||||
@@ -189,13 +164,6 @@ public:
|
|||||||
|
|
||||||
bool isCompressing() const;
|
bool isCompressing() const;
|
||||||
|
|
||||||
bool isInEditor() const;
|
|
||||||
void setInEditor(bool value);
|
|
||||||
|
|
||||||
QString chatTitle() const;
|
|
||||||
|
|
||||||
Q_INVOKABLE void requestNewChat();
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
void copyToClipboard(const QString &text);
|
void copyToClipboard(const QString &text);
|
||||||
@@ -222,7 +190,6 @@ signals:
|
|||||||
|
|
||||||
void lastErrorMessageChanged();
|
void lastErrorMessageChanged();
|
||||||
void lastInfoMessageChanged();
|
void lastInfoMessageChanged();
|
||||||
void sendShortcutTextChanged();
|
|
||||||
void activeRulesChanged();
|
void activeRulesChanged();
|
||||||
void activeRulesCountChanged();
|
void activeRulesCountChanged();
|
||||||
|
|
||||||
@@ -242,34 +209,15 @@ signals:
|
|||||||
void compressionCompleted(const QString &compressedChatPath);
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
void compressionFailed(const QString &error);
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
void isInEditorChanged();
|
|
||||||
void chatTitleChanged();
|
|
||||||
|
|
||||||
void openFilesChanged();
|
void openFilesChanged();
|
||||||
|
|
||||||
void closeHostRequested();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString computeChatTitle() const;
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
void triggerOpenChatCommand(Utils::Id commandId);
|
QString getChatsHistoryDir() const;
|
||||||
void handOffSession();
|
QString getSuggestedFileName() const;
|
||||||
bool deferSendForAutoCompress(
|
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||||
const QString &message,
|
|
||||||
const QStringList &attachments,
|
|
||||||
const QStringList &linkedFiles,
|
|
||||||
bool useTools,
|
|
||||||
bool useThinking);
|
|
||||||
void dispatchSend(
|
|
||||||
const QString &message,
|
|
||||||
const QStringList &attachments,
|
|
||||||
const QStringList &linkedFiles,
|
|
||||||
bool useTools,
|
|
||||||
bool useThinking);
|
|
||||||
bool hasImageAttachments(const QStringList &attachments) const;
|
bool hasImageAttachments(const QStringList &attachments) const;
|
||||||
|
|
||||||
SessionFileRegistry *sessionFileRegistry() const;
|
|
||||||
Skills::SkillsManager *skillsManager() const;
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
PluginLLMCore::PromptProviderChat m_promptProvider;
|
PluginLLMCore::PromptProviderChat m_promptProvider;
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
@@ -278,36 +226,28 @@ private:
|
|||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
QStringList m_linkedFiles;
|
QStringList m_linkedFiles;
|
||||||
|
int m_messageTokensCount{0};
|
||||||
struct PendingSend {
|
int m_inputTokensCount{0};
|
||||||
QString message;
|
|
||||||
QStringList attachments;
|
|
||||||
QStringList linkedFiles;
|
|
||||||
bool useTools = false;
|
|
||||||
bool useThinking = false;
|
|
||||||
bool active = false;
|
|
||||||
};
|
|
||||||
PendingSend m_pendingSend;
|
|
||||||
bool m_isSyncOpenFiles;
|
bool m_isSyncOpenFiles;
|
||||||
bool m_isInEditor = false;
|
|
||||||
mutable QString m_cachedChatTitle;
|
|
||||||
QList<Core::IEditor *> m_currentEditors;
|
QList<Core::IEditor *> m_currentEditors;
|
||||||
bool m_isRequestInProgress;
|
bool m_isRequestInProgress;
|
||||||
QString m_lastErrorMessage;
|
QString m_lastErrorMessage;
|
||||||
QVariantList m_activeRules;
|
QVariantList m_activeRules;
|
||||||
|
|
||||||
|
QString m_currentMessageRequestId;
|
||||||
|
int m_currentMessageTotalEdits{0};
|
||||||
|
int m_currentMessageAppliedEdits{0};
|
||||||
|
int m_currentMessagePendingEdits{0};
|
||||||
|
int m_currentMessageRejectedEdits{0};
|
||||||
QString m_lastInfoMessage;
|
QString m_lastInfoMessage;
|
||||||
|
|
||||||
|
QStringList m_availableConfigurations;
|
||||||
|
QString m_currentConfiguration;
|
||||||
|
|
||||||
|
QStringList m_availableAgentRoles;
|
||||||
|
QString m_currentAgentRole;
|
||||||
|
|
||||||
ChatCompressor *m_chatCompressor;
|
ChatCompressor *m_chatCompressor;
|
||||||
AgentRoleController *m_agentRoleController;
|
|
||||||
ChatConfigurationController *m_configurationController;
|
|
||||||
FileEditController *m_fileEditController;
|
|
||||||
InputTokenCounter *m_tokenCounter;
|
|
||||||
ChatHistoryStore *m_historyStore;
|
|
||||||
mutable QPointer<SessionFileRegistry> m_sessionFileRegistry;
|
|
||||||
mutable bool m_sessionFileRegistryResolved = false;
|
|
||||||
mutable QPointer<Skills::SkillsManager> m_skillsManager;
|
|
||||||
mutable bool m_skillsManagerResolved = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
@@ -81,15 +80,6 @@ QJsonObject ChatSerializer::serializeMessage(
|
|||||||
messageObj["signature"] = message.signature;
|
messageObj["signature"] = message.signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
|
||||||
if (!message.toolName.isEmpty())
|
|
||||||
messageObj["toolName"] = message.toolName;
|
|
||||||
if (!message.toolArguments.isEmpty())
|
|
||||||
messageObj["toolArguments"] = message.toolArguments;
|
|
||||||
if (!message.toolResult.isEmpty())
|
|
||||||
messageObj["toolResult"] = message.toolResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message.attachments.isEmpty()) {
|
if (!message.attachments.isEmpty()) {
|
||||||
QJsonArray attachmentsArray;
|
QJsonArray attachmentsArray;
|
||||||
for (const auto &attachment : message.attachments) {
|
for (const auto &attachment : message.attachments) {
|
||||||
@@ -113,17 +103,6 @@ QJsonObject ChatSerializer::serializeMessage(
|
|||||||
messageObj["images"] = imagesArray;
|
messageObj["images"] = imagesArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.promptTokens > 0 || message.completionTokens > 0) {
|
|
||||||
QJsonObject usageObj;
|
|
||||||
usageObj["promptTokens"] = message.promptTokens;
|
|
||||||
usageObj["completionTokens"] = message.completionTokens;
|
|
||||||
if (message.cachedPromptTokens > 0)
|
|
||||||
usageObj["cachedPromptTokens"] = message.cachedPromptTokens;
|
|
||||||
if (message.reasoningTokens > 0)
|
|
||||||
usageObj["reasoningTokens"] = message.reasoningTokens;
|
|
||||||
messageObj["usage"] = usageObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageObj;
|
return messageObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +115,6 @@ ChatModel::Message ChatSerializer::deserializeMessage(
|
|||||||
message.id = json["id"].toString();
|
message.id = json["id"].toString();
|
||||||
message.isRedacted = json["isRedacted"].toBool(false);
|
message.isRedacted = json["isRedacted"].toBool(false);
|
||||||
message.signature = json["signature"].toString();
|
message.signature = json["signature"].toString();
|
||||||
message.toolName = json["toolName"].toString();
|
|
||||||
message.toolArguments = json["toolArguments"].toObject();
|
|
||||||
message.toolResult = json["toolResult"].toString();
|
|
||||||
|
|
||||||
if (json.contains("attachments")) {
|
if (json.contains("attachments")) {
|
||||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
QJsonArray attachmentsArray = json["attachments"].toArray();
|
||||||
@@ -163,14 +139,6 @@ ChatModel::Message ChatSerializer::deserializeMessage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.contains("usage")) {
|
|
||||||
const QJsonObject usageObj = json["usage"].toObject();
|
|
||||||
message.promptTokens = usageObj["promptTokens"].toInt();
|
|
||||||
message.completionTokens = usageObj["completionTokens"].toInt();
|
|
||||||
message.cachedPromptTokens = usageObj["cachedPromptTokens"].toInt();
|
|
||||||
message.reasoningTokens = usageObj["reasoningTokens"].toInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,10 +180,6 @@ bool ChatSerializer::deserializeChat(
|
|||||||
message.images,
|
message.images,
|
||||||
message.isRedacted,
|
message.isRedacted,
|
||||||
message.signature);
|
message.signature);
|
||||||
if (message.role == ChatModel::ChatRole::Tool) {
|
|
||||||
model->setToolMessageData(
|
|
||||||
message.id, message.toolName, message.toolArguments, message.toolResult);
|
|
||||||
}
|
|
||||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
||||||
.arg(message.images.size())
|
.arg(message.images.size())
|
||||||
.arg(message.isRedacted)
|
.arg(message.isRedacted)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatUtils.h"
|
#include "ChatUtils.h"
|
||||||
|
|
||||||
@@ -20,34 +19,22 @@ QString ChatUtils::getSafeMarkdownText(const QString &text) const
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool needsSanitization = false;
|
||||||
|
for (const QChar &ch : text) {
|
||||||
|
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
|
||||||
|
needsSanitization = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsSanitization) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
QString safeText;
|
QString safeText;
|
||||||
safeText.reserve(text.size() + 16);
|
safeText.reserve(text.size());
|
||||||
|
|
||||||
bool inFenced = false;
|
|
||||||
bool inInline = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < text.size(); ++i) {
|
|
||||||
const QChar ch = text[i];
|
|
||||||
|
|
||||||
if (!inInline && i + 2 < text.size()
|
|
||||||
&& text[i] == '`' && text[i + 1] == '`' && text[i + 2] == '`') {
|
|
||||||
safeText.append(QStringLiteral("```"));
|
|
||||||
inFenced = !inFenced;
|
|
||||||
i += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inFenced && ch == '`') {
|
|
||||||
safeText.append(ch);
|
|
||||||
inInline = !inInline;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inFenced && !inInline && ch == '<') {
|
|
||||||
safeText.append(QStringLiteral("<"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (QChar ch : text) {
|
||||||
if (ch.isNull()) {
|
if (ch.isNull()) {
|
||||||
safeText.append(' ');
|
safeText.append(' ');
|
||||||
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
|
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatView.hpp"
|
#include "ChatView.hpp"
|
||||||
|
|
||||||
#include <QQmlComponent>
|
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QQuickItem>
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QVariantMap>
|
#include <QVariantMap>
|
||||||
|
|
||||||
#include <coreplugin/actionmanager/actionmanager.h>
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
#include <coreplugin/actionmanager/command.h>
|
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
|
|
||||||
#include "ChatRootView.hpp"
|
|
||||||
#include "QodeAssistConstants.hpp"
|
|
||||||
#include "SessionFileRegistry.hpp"
|
|
||||||
#include "sources/skills/SkillsManager.hpp"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
|
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
|
||||||
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
|
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
|
||||||
@@ -28,63 +19,28 @@ constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::Win
|
|||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatView::ChatView(
|
ChatView::ChatView()
|
||||||
QQmlEngine *engine,
|
: m_isPin(false)
|
||||||
SessionFileRegistry *sessionFileRegistry,
|
|
||||||
Skills::SkillsManager *skillsManager)
|
|
||||||
: QQuickView{engine, nullptr}
|
|
||||||
, m_isPin(false)
|
|
||||||
{
|
{
|
||||||
setTitle("QodeAssist Chat");
|
setTitle("QodeAssist Chat");
|
||||||
/// @note setup quick view content
|
engine()->rootContext()->setContextProperty("_chatview", this);
|
||||||
{
|
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||||
auto context = new QQmlContext{engine, this};
|
|
||||||
context->setContextProperty("_chatview", this);
|
|
||||||
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
|
|
||||||
context->setContextProperty("skillsManager", skillsManager);
|
|
||||||
|
|
||||||
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
|
|
||||||
auto rootItem = component->create(context);
|
|
||||||
|
|
||||||
setContent(component->url(), component, rootItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto rootView = qobject_cast<ChatRootView *>(rootObject())) {
|
|
||||||
connect(
|
|
||||||
rootView,
|
|
||||||
&ChatRootView::closeHostRequested,
|
|
||||||
this,
|
|
||||||
&QWindow::close,
|
|
||||||
Qt::QueuedConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
setResizeMode(QQuickView::SizeRootObjectToView);
|
setResizeMode(QQuickView::SizeRootObjectToView);
|
||||||
setMinimumSize({400, 300});
|
setMinimumSize({400, 300});
|
||||||
setFlags(baseFlags);
|
setFlags(baseFlags);
|
||||||
|
|
||||||
bindCommandShortcut("QodeAssist.CloseChatView", [this] { close(); });
|
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
|
||||||
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_SEND_MESSAGE, [this] {
|
m_closeShortcut = new QShortcut(action->keySequence(), this);
|
||||||
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
|
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
|
||||||
});
|
|
||||||
bindCommandShortcut(Constants::QODE_ASSIST_CHAT_CLEAR_SESSION, [this] {
|
|
||||||
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
|
||||||
});
|
|
||||||
|
|
||||||
restoreSettings();
|
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
|
||||||
|
if (m_closeShortcut) {
|
||||||
|
m_closeShortcut->setKey(action->keySequence());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatView::bindCommandShortcut(Utils::Id commandId,
|
restoreSettings();
|
||||||
const std::function<void()> &onActivated)
|
|
||||||
{
|
|
||||||
auto command = Core::ActionManager::command(commandId);
|
|
||||||
if (!command)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto shortcut = new QShortcut(command->keySequence(), this);
|
|
||||||
connect(shortcut, &QShortcut::activated, this, onActivated);
|
|
||||||
connect(command, &Core::Command::keySequenceChanged, shortcut, [command, shortcut]() {
|
|
||||||
shortcut->setKey(command->keySequence());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatView::closeEvent(QCloseEvent *event)
|
void ChatView::closeEvent(QCloseEvent *event)
|
||||||
|
|||||||
@@ -1,33 +1,19 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include <utils/id.h>
|
|
||||||
|
|
||||||
#include <QQuickView>
|
#include <QQuickView>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
|
||||||
class SkillsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class SessionFileRegistry;
|
|
||||||
|
|
||||||
class ChatView : public QQuickView
|
class ChatView : public QQuickView
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
|
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
|
||||||
public:
|
public:
|
||||||
ChatView(
|
ChatView();
|
||||||
QQmlEngine *engine,
|
|
||||||
SessionFileRegistry *sessionFileRegistry,
|
|
||||||
Skills::SkillsManager *skillsManager);
|
|
||||||
|
|
||||||
bool isPin() const;
|
bool isPin() const;
|
||||||
void setIsPin(bool newIsPin);
|
void setIsPin(bool newIsPin);
|
||||||
@@ -41,9 +27,9 @@ protected:
|
|||||||
private:
|
private:
|
||||||
void saveSettings();
|
void saveSettings();
|
||||||
void restoreSettings();
|
void restoreSettings();
|
||||||
void bindCommandShortcut(Utils::Id commandId, const std::function<void()> &onActivated);
|
|
||||||
|
|
||||||
bool m_isPin;
|
bool m_isPin;
|
||||||
|
QShortcut *m_closeShortcut;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,68 +1,18 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ChatWidget.hpp"
|
#include "ChatWidget.hpp"
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QMouseEvent>
|
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QQuickItem>
|
|
||||||
|
|
||||||
#include <coreplugin/icontext.h>
|
|
||||||
#include <coreplugin/icore.h>
|
|
||||||
|
|
||||||
#include "QodeAssistConstants.hpp"
|
|
||||||
#include "SessionFileRegistry.hpp"
|
|
||||||
#include "sources/skills/SkillsManager.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ChatWidget::ChatWidget(
|
ChatWidget::ChatWidget(QWidget *parent)
|
||||||
QQmlEngine *engine,
|
: QQuickWidget(parent)
|
||||||
SessionFileRegistry *sessionFileRegistry,
|
|
||||||
Skills::SkillsManager *skillsManager,
|
|
||||||
bool registerOwnContext,
|
|
||||||
QWidget *parent)
|
|
||||||
: QQuickWidget{engine, parent}
|
|
||||||
{
|
{
|
||||||
/// @note setup quick view content
|
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
|
||||||
{
|
|
||||||
auto context = new QQmlContext{engine, this};
|
|
||||||
context->setContextProperty("sessionFileRegistry", sessionFileRegistry);
|
|
||||||
context->setContextProperty("skillsManager", skillsManager);
|
|
||||||
auto component = new QQmlComponent{engine, QUrl{"qrc:/qt/qml/ChatView/qml/RootItem.qml"}, this};
|
|
||||||
auto rootItem = component->create(context);
|
|
||||||
|
|
||||||
setContent(component->url(), component, rootItem);
|
|
||||||
}
|
|
||||||
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||||
setFocusPolicy(Qt::StrongFocus);
|
|
||||||
|
|
||||||
setAttribute(Qt::WA_NoMousePropagation, true);
|
|
||||||
|
|
||||||
if (registerOwnContext) {
|
|
||||||
auto ideContext = new Core::IContext{this};
|
|
||||||
ideContext->setWidget(this);
|
|
||||||
ideContext->setContext(Core::Context{Constants::QODE_ASSIST_CHAT_CONTEXT});
|
|
||||||
Core::ICore::addContextObject(ideContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatWidget::focusInEvent(QFocusEvent *event)
|
|
||||||
{
|
|
||||||
QQuickWidget::focusInEvent(event);
|
|
||||||
if (rootObject())
|
|
||||||
QMetaObject::invokeMethod(rootObject(), "focusInput");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatWidget::mousePressEvent(QMouseEvent *event)
|
|
||||||
{
|
|
||||||
if (!hasFocus())
|
|
||||||
setFocus(Qt::MouseFocusReason);
|
|
||||||
|
|
||||||
QQuickWidget::mousePressEvent(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatWidget::clear()
|
void ChatWidget::clear()
|
||||||
@@ -74,35 +24,4 @@ void ChatWidget::scrollToBottom()
|
|||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatWidget::focusInput()
|
|
||||||
{
|
|
||||||
setFocus(Qt::OtherFocusReason);
|
|
||||||
QMetaObject::invokeMethod(rootObject(), "focusInput");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatWidget::isChatFocused() const
|
|
||||||
{
|
|
||||||
return hasFocus() || (rootObject() && rootObject()->hasActiveFocus());
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatWidget::sendMessage()
|
|
||||||
{
|
|
||||||
QMetaObject::invokeMethod(rootObject(), "sendChatMessage");
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatWidget::clearSession()
|
|
||||||
{
|
|
||||||
QMetaObject::invokeMethod(rootObject(), "clearChat");
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatWidget *ChatWidget::focusedInstance()
|
|
||||||
{
|
|
||||||
for (QWidget *widget = QApplication::focusWidget(); widget;
|
|
||||||
widget = widget->parentWidget()) {
|
|
||||||
if (auto chatWidget = qobject_cast<ChatWidget *>(widget))
|
|
||||||
return chatWidget;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,49 +1,25 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QtQuickWidgets/QtQuickWidgets>
|
#include <QtQuickWidgets/QtQuickWidgets>
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
|
||||||
class SkillsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class SessionFileRegistry;
|
|
||||||
|
|
||||||
class ChatWidget : public QQuickWidget
|
class ChatWidget : public QQuickWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChatWidget(
|
explicit ChatWidget(QWidget *parent = nullptr);
|
||||||
QQmlEngine *engine,
|
|
||||||
SessionFileRegistry *sessionFileRegistry,
|
|
||||||
Skills::SkillsManager *skillsManager,
|
|
||||||
bool registerOwnContext = true,
|
|
||||||
QWidget *parent = nullptr);
|
|
||||||
~ChatWidget() = default;
|
~ChatWidget() = default;
|
||||||
|
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
Q_INVOKABLE void scrollToBottom();
|
Q_INVOKABLE void scrollToBottom();
|
||||||
Q_INVOKABLE void focusInput();
|
|
||||||
|
|
||||||
void sendMessage();
|
|
||||||
void clearSession();
|
|
||||||
|
|
||||||
bool isChatFocused() const;
|
|
||||||
|
|
||||||
static ChatWidget *focusedInstance();
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void clearPressed();
|
void clearPressed();
|
||||||
|
|
||||||
protected:
|
|
||||||
void focusInEvent(QFocusEvent *event) override;
|
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QMimeDatabase>
|
#include <QMimeDatabase>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
@@ -30,36 +28,27 @@
|
|||||||
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
|
|
||||||
#include "tools/ReadOriginalHistoryTool.hpp"
|
|
||||||
#include "tools/TodoTool.hpp"
|
#include "tools/TodoTool.hpp"
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
#include "SkillsSettings.hpp"
|
|
||||||
#include "ToolsSettings.hpp"
|
#include "ToolsSettings.hpp"
|
||||||
#include <RulesLoader.hpp>
|
#include <RulesLoader.hpp>
|
||||||
#include <context/ChangesManager.h>
|
#include <context/ChangesManager.h>
|
||||||
#include <sources/skills/SkillsManager.hpp>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ClientInterface::ClientInterface(
|
ClientInterface::ClientInterface(
|
||||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_promptProvider(promptProvider)
|
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
|
, m_promptProvider(promptProvider)
|
||||||
, m_contextManager(new Context::ContextManager(this))
|
, m_contextManager(new Context::ContextManager(this))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ClientInterface::setSkillsManager(Skills::SkillsManager *skillsManager)
|
|
||||||
{
|
|
||||||
m_skillsManager = skillsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientInterface::~ClientInterface()
|
ClientInterface::~ClientInterface()
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
@@ -72,11 +61,6 @@ void ClientInterface::sendMessage(
|
|||||||
bool useTools,
|
bool useTools,
|
||||||
bool useThinking)
|
bool useThinking)
|
||||||
{
|
{
|
||||||
if (message.trimmed().isEmpty() && attachments.isEmpty()) {
|
|
||||||
LOG_MESSAGE("Ignoring empty chat message");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
m_accumulatedResponses.clear();
|
||||||
|
|
||||||
@@ -175,20 +159,13 @@ void ClientInterface::sendMessage(
|
|||||||
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||||
|
|
||||||
if (project) {
|
if (project) {
|
||||||
systemPrompt += QString("\n# Active project: %1").arg(project->displayName());
|
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
|
||||||
systemPrompt += QString(
|
systemPrompt += QString("\n# Active Project path: %1")
|
||||||
"\n# Project source root: %1"
|
|
||||||
"\n# All new source files, headers, QML and CMake edits MUST be "
|
|
||||||
"created or modified under this directory. Use absolute paths "
|
|
||||||
"rooted here, or project-relative paths.")
|
|
||||||
.arg(project->projectDirectory().toUrlishString());
|
.arg(project->projectDirectory().toUrlishString());
|
||||||
|
|
||||||
if (auto target = project->activeTarget()) {
|
if (auto target = project->activeTarget()) {
|
||||||
if (auto buildConfig = target->activeBuildConfiguration()) {
|
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||||
systemPrompt
|
systemPrompt += QString("\n# Active Build directory: %1")
|
||||||
+= QString(
|
|
||||||
"\n# Build output directory (compiler artifacts only — do NOT "
|
|
||||||
"create or edit source files here): %1")
|
|
||||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,85 +180,15 @@ void ClientInterface::sendMessage(
|
|||||||
systemPrompt += QString("\n# No active project in IDE");
|
systemPrompt += QString("\n# No active project in IDE");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_skillsManager && Settings::skillsSettings().enableSkills()) {
|
|
||||||
QStringList projectSkillDirs;
|
|
||||||
if (project) {
|
|
||||||
Settings::ProjectSettings projectSettings(project);
|
|
||||||
projectSkillDirs = Settings::SkillsSettings::splitLines(
|
|
||||||
projectSettings.projectSkillDirs());
|
|
||||||
}
|
|
||||||
m_skillsManager->configure(
|
|
||||||
project ? project->projectDirectory().toFSPathString() : QString(),
|
|
||||||
Settings::SkillsSettings::splitPaths(
|
|
||||||
Settings::skillsSettings().globalSkillRoots()),
|
|
||||||
projectSkillDirs);
|
|
||||||
|
|
||||||
const QString alwaysOnSkills = m_skillsManager->alwaysOnBodies();
|
|
||||||
if (!alwaysOnSkills.isEmpty())
|
|
||||||
systemPrompt += QString("\n\n") + alwaysOnSkills;
|
|
||||||
|
|
||||||
const QString skillsCatalog = m_skillsManager->catalogText();
|
|
||||||
if (!skillsCatalog.isEmpty())
|
|
||||||
systemPrompt += QString("\n\n") + skillsCatalog;
|
|
||||||
|
|
||||||
static const QRegularExpression skillCommand(
|
|
||||||
QStringLiteral("(?:^|\\s)/([a-z0-9][a-z0-9-]*)"));
|
|
||||||
QStringList invokedSkillNames;
|
|
||||||
auto skillMatch = skillCommand.globalMatch(message);
|
|
||||||
while (skillMatch.hasNext()) {
|
|
||||||
const QString skillName = skillMatch.next().captured(1);
|
|
||||||
if (invokedSkillNames.contains(skillName))
|
|
||||||
continue;
|
|
||||||
const auto invokedSkill = m_skillsManager->findByName(skillName);
|
|
||||||
if (invokedSkill && !invokedSkill->body.isEmpty()) {
|
|
||||||
invokedSkillNames << skillName;
|
|
||||||
systemPrompt += QString("\n\n# Invoked Skill: %1\n\n%2")
|
|
||||||
.arg(invokedSkill->name, invokedSkill->body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!linkedFiles.isEmpty()) {
|
if (!linkedFiles.isEmpty()) {
|
||||||
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
|
||||||
}
|
}
|
||||||
context.systemPrompt = systemPrompt;
|
context.systemPrompt = systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool toolHistory = promptTemplate->supportsToolHistory();
|
|
||||||
|
|
||||||
QVector<PluginLLMCore::Message> messages;
|
QVector<PluginLLMCore::Message> messages;
|
||||||
int toolCallMsgIdx = -1;
|
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||||
if (msg.role == ChatModel::ChatRole::Tool) {
|
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
||||||
if (!toolHistory || msg.toolName.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolCallMsgIdx < 0) {
|
|
||||||
PluginLLMCore::Message assistantCall;
|
|
||||||
assistantCall.role = "assistant";
|
|
||||||
messages.append(assistantCall);
|
|
||||||
toolCallMsgIdx = messages.size() - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLLMCore::ToolCall call;
|
|
||||||
call.id = msg.id;
|
|
||||||
call.name = msg.toolName;
|
|
||||||
call.arguments = msg.toolArguments;
|
|
||||||
messages[toolCallMsgIdx].toolCalls.append(call);
|
|
||||||
|
|
||||||
PluginLLMCore::Message toolResult;
|
|
||||||
toolResult.role = "tool";
|
|
||||||
toolResult.toolCallId = msg.id;
|
|
||||||
toolResult.toolName = msg.toolName;
|
|
||||||
toolResult.content = msg.toolResult;
|
|
||||||
messages.append(toolResult);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
toolCallMsgIdx = -1;
|
|
||||||
|
|
||||||
if (msg.role == ChatModel::ChatRole::FileEdit) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,9 +245,6 @@ void ClientInterface::sendMessage(
|
|||||||
provider->client()->setMaxToolContinuations(
|
provider->client()->setMaxToolContinuations(
|
||||||
Settings::toolsSettings().maxToolContinuations());
|
Settings::toolsSettings().maxToolContinuations());
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
provider->client(),
|
||||||
&::LLMQore::BaseClient::chunkReceived,
|
&::LLMQore::BaseClient::chunkReceived,
|
||||||
@@ -353,12 +257,6 @@ void ClientInterface::sendMessage(
|
|||||||
this,
|
this,
|
||||||
&ClientInterface::handleFullResponse,
|
&ClientInterface::handleFullResponse,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
|
||||||
this,
|
|
||||||
&ClientInterface::handleRequestFinalized,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
provider->client(),
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
@@ -391,7 +289,7 @@ void ClientInterface::sendMessage(
|
|||||||
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||||
QJsonObject request{{"id", requestId}};
|
QJsonObject request{{"id", requestId}};
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider, !toolHistory};
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
|
||||||
emit requestStarted(requestId);
|
emit requestStarted(requestId);
|
||||||
|
|
||||||
@@ -401,10 +299,6 @@ void ClientInterface::sendMessage(
|
|||||||
provider->toolsManager()->tool("todo_tool"))) {
|
provider->toolsManager()->tool("todo_tool"))) {
|
||||||
todoTool->setCurrentSessionId(m_chatFilePath);
|
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||||
}
|
}
|
||||||
if (auto *historyTool = qobject_cast<QodeAssist::Tools::ReadOriginalHistoryTool *>(
|
|
||||||
provider->toolsManager()->tool("read_original_history"))) {
|
|
||||||
historyTool->setCurrentSessionId(m_chatFilePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,29 +449,6 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
|||||||
m_awaitingContinuation.remove(requestId);
|
m_awaitingContinuation.remove(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId))
|
|
||||||
return;
|
|
||||||
if (!info.usage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
|
||||||
m_chatModel->setMessageUsage(
|
|
||||||
requestId, u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
|
||||||
|
|
||||||
emit messageUsageReceived(
|
|
||||||
u.promptTokens, u.completionTokens, u.cachedPromptTokens, u.reasoningTokens);
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Chat usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||||
{
|
{
|
||||||
auto it = m_activeRequests.find(requestId);
|
auto it = m_activeRequests.find(requestId);
|
||||||
@@ -614,21 +485,14 @@ void ClientInterface::handleThinkingBlockReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleToolExecutionStarted(
|
void ClientInterface::handleToolExecutionStarted(
|
||||||
const QString &requestId,
|
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &arguments)
|
|
||||||
{
|
{
|
||||||
const auto requestIt = m_activeRequests.constFind(requestId);
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
if (requestIt == m_activeRequests.constEnd()) {
|
|
||||||
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestIt->dropPreToolText) {
|
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
||||||
m_chatModel->dropTrailingAssistantMessage(requestId);
|
|
||||||
}
|
|
||||||
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName, arguments);
|
|
||||||
m_awaitingContinuation.insert(requestId);
|
m_awaitingContinuation.insert(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -12,13 +11,8 @@
|
|||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "Provider.hpp"
|
#include "Provider.hpp"
|
||||||
#include "pluginllmcore/IPromptProvider.hpp"
|
#include "pluginllmcore/IPromptProvider.hpp"
|
||||||
#include <LLMQore/BaseClient.hpp>
|
|
||||||
#include <context/ContextManager.hpp>
|
#include <context/ContextManager.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Skills {
|
|
||||||
class SkillsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
class ClientInterface : public QObject
|
class ClientInterface : public QObject
|
||||||
@@ -30,8 +24,6 @@ public:
|
|||||||
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
|
||||||
~ClientInterface();
|
~ClientInterface();
|
||||||
|
|
||||||
void setSkillsManager(Skills::SkillsManager *skillsManager);
|
|
||||||
|
|
||||||
void sendMessage(
|
void sendMessage(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QList<QString> &attachments = {},
|
const QList<QString> &attachments = {},
|
||||||
@@ -50,21 +42,15 @@ signals:
|
|||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
void messageReceivedCompletely();
|
void messageReceivedCompletely();
|
||||||
void requestStarted(const QString &requestId);
|
void requestStarted(const QString &requestId);
|
||||||
void messageUsageReceived(
|
|
||||||
int promptTokens, int completionTokens, int cachedPromptTokens, int reasoningTokens);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||||
void handleRequestFinalized(const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||||
void handleThinkingBlockReceived(
|
void handleThinkingBlockReceived(
|
||||||
const QString &requestId, const QString &thinking, const QString &signature);
|
const QString &requestId, const QString &thinking, const QString &signature);
|
||||||
void handleToolExecutionStarted(
|
void handleToolExecutionStarted(
|
||||||
const QString &requestId,
|
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||||
const QString &toolId,
|
|
||||||
const QString &toolName,
|
|
||||||
const QJsonObject &arguments);
|
|
||||||
void handleToolExecutionCompleted(
|
void handleToolExecutionCompleted(
|
||||||
const QString &requestId,
|
const QString &requestId,
|
||||||
const QString &toolId,
|
const QString &toolId,
|
||||||
@@ -85,13 +71,11 @@ private:
|
|||||||
{
|
{
|
||||||
QJsonObject originalRequest;
|
QJsonObject originalRequest;
|
||||||
PluginLLMCore::Provider *provider;
|
PluginLLMCore::Provider *provider;
|
||||||
bool dropPreToolText = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
Context::ContextManager *m_contextManager;
|
Context::ContextManager *m_contextManager;
|
||||||
Skills::SkillsManager *m_skillsManager = nullptr;
|
|
||||||
QString m_chatFilePath;
|
QString m_chatFilePath;
|
||||||
|
|
||||||
QHash<QString, RequestContext> m_activeRequests;
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FileEditController.hpp"
|
|
||||||
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
|
||||||
#include <texteditor/texteditor.h>
|
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "Logger.hpp"
|
|
||||||
#include "context/ChangesManager.h"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_chatModel(chatModel)
|
|
||||||
{
|
|
||||||
auto &changes = Context::ChangesManager::instance();
|
|
||||||
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
|
||||||
updateStats();
|
|
||||||
});
|
|
||||||
connect(&changes, &Context::ChangesManager::fileEditApplied, this, [this](const QString &) {
|
|
||||||
updateStats();
|
|
||||||
});
|
|
||||||
connect(&changes, &Context::ChangesManager::fileEditRejected, this, [this](const QString &) {
|
|
||||||
updateStats();
|
|
||||||
});
|
|
||||||
connect(&changes, &Context::ChangesManager::fileEditUndone, this, [this](const QString &) {
|
|
||||||
updateStats();
|
|
||||||
});
|
|
||||||
connect(&changes, &Context::ChangesManager::fileEditArchived, this, [this](const QString &) {
|
|
||||||
updateStats();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::setCurrentRequestId(const QString &requestId)
|
|
||||||
{
|
|
||||||
if (!m_currentRequestId.isEmpty()) {
|
|
||||||
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentRequestId));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_currentRequestId = requestId;
|
|
||||||
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::clearCurrentRequestId()
|
|
||||||
{
|
|
||||||
m_currentRequestId.clear();
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
int FileEditController::totalEdits() const
|
|
||||||
{
|
|
||||||
return m_totalEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
int FileEditController::appliedEdits() const
|
|
||||||
{
|
|
||||||
return m_appliedEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
int FileEditController::pendingEdits() const
|
|
||||||
{
|
|
||||||
return m_pendingEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
int FileEditController::rejectedEdits() const
|
|
||||||
{
|
|
||||||
return m_rejectedEdits;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::applyFileEdit(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
|
||||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
|
||||||
emit infoMessage(QString("File edit applied successfully"));
|
|
||||||
updateFileEditStatus(editId, "applied");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
emit errorOccurred(
|
|
||||||
edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to apply file edit")
|
|
||||||
: QString("Failed to apply file edit: %1").arg(edit.statusMessage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::rejectFileEdit(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
|
||||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
|
||||||
emit infoMessage(QString("File edit rejected"));
|
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
emit errorOccurred(
|
|
||||||
edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to reject file edit")
|
|
||||||
: QString("Failed to reject file edit: %1").arg(edit.statusMessage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::undoFileEdit(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
|
||||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
|
||||||
emit infoMessage(QString("File edit undone successfully"));
|
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
emit errorOccurred(
|
|
||||||
edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to undo file edit")
|
|
||||||
: QString("Failed to undo file edit: %1").arg(edit.statusMessage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::openFileEditInEditor(const QString &editId)
|
|
||||||
{
|
|
||||||
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
|
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (edit.editId.isEmpty()) {
|
|
||||||
emit errorOccurred(QString("File edit not found: %1").arg(editId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
|
|
||||||
|
|
||||||
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
|
|
||||||
if (!editor) {
|
|
||||||
emit errorOccurred(QString("Failed to open file in editor: %1").arg(edit.filePath));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
|
|
||||||
if (textEditor && textEditor->editorWidget()) {
|
|
||||||
QTextDocument *doc = textEditor->editorWidget()->document();
|
|
||||||
if (doc) {
|
|
||||||
QString currentContent = doc->toPlainText();
|
|
||||||
int position = -1;
|
|
||||||
|
|
||||||
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
|
||||||
position = currentContent.indexOf(edit.newContent);
|
|
||||||
} else if (!edit.oldContent.isEmpty()) {
|
|
||||||
position = currentContent.indexOf(edit.oldContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position >= 0) {
|
|
||||||
QTextCursor cursor(doc);
|
|
||||||
cursor.setPosition(position);
|
|
||||||
textEditor->editorWidget()->setTextCursor(cursor);
|
|
||||||
textEditor->editorWidget()->centerCursor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
|
|
||||||
{
|
|
||||||
auto messages = m_chatModel->getChatHistory();
|
|
||||||
for (int i = 0; i < messages.size(); ++i) {
|
|
||||||
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
|
||||||
QString content = messages[i].content;
|
|
||||||
|
|
||||||
const QString marker = "QODEASSIST_FILE_EDIT:";
|
|
||||||
int markerPos = content.indexOf(marker);
|
|
||||||
|
|
||||||
QString jsonStr = content;
|
|
||||||
if (markerPos >= 0) {
|
|
||||||
jsonStr = content.mid(markerPos + marker.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
||||||
if (doc.isObject()) {
|
|
||||||
QJsonObject obj = doc.object();
|
|
||||||
obj["status"] = status;
|
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (!edit.statusMessage.isEmpty()) {
|
|
||||||
obj["status_message"] = edit.statusMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString updatedContent = marker
|
|
||||||
+ QString::fromUtf8(
|
|
||||||
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
|
||||||
m_chatModel->updateMessageContent(editId, updatedContent);
|
|
||||||
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::applyAllForCurrentMessage()
|
|
||||||
{
|
|
||||||
if (m_currentRequestId.isEmpty()) {
|
|
||||||
emit errorOccurred(QString("No active message with file edits"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentRequestId));
|
|
||||||
|
|
||||||
QString errorMsg;
|
|
||||||
bool success = Context::ChangesManager::instance()
|
|
||||||
.reapplyAllEditsForRequest(m_currentRequestId, &errorMsg);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
emit infoMessage(QString("All file edits applied successfully"));
|
|
||||||
} else {
|
|
||||||
emit errorOccurred(
|
|
||||||
errorMsg.isEmpty()
|
|
||||||
? QString("Failed to apply some file edits")
|
|
||||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::undoAllForCurrentMessage()
|
|
||||||
{
|
|
||||||
if (m_currentRequestId.isEmpty()) {
|
|
||||||
emit errorOccurred(QString("No active message with file edits"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentRequestId));
|
|
||||||
|
|
||||||
QString errorMsg;
|
|
||||||
bool success = Context::ChangesManager::instance()
|
|
||||||
.undoAllEditsForRequest(m_currentRequestId, &errorMsg);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
emit infoMessage(QString("All file edits undone successfully"));
|
|
||||||
} else {
|
|
||||||
emit errorOccurred(
|
|
||||||
errorMsg.isEmpty()
|
|
||||||
? QString("Failed to undo some file edits")
|
|
||||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FileEditController::updateStats()
|
|
||||||
{
|
|
||||||
if (m_currentRequestId.isEmpty()) {
|
|
||||||
if (m_totalEdits != 0 || m_appliedEdits != 0 || m_pendingEdits != 0
|
|
||||||
|| m_rejectedEdits != 0) {
|
|
||||||
m_totalEdits = 0;
|
|
||||||
m_appliedEdits = 0;
|
|
||||||
m_pendingEdits = 0;
|
|
||||||
m_rejectedEdits = 0;
|
|
||||||
emit statsChanged();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
|
||||||
|
|
||||||
int total = edits.size();
|
|
||||||
int applied = 0;
|
|
||||||
int pending = 0;
|
|
||||||
int rejected = 0;
|
|
||||||
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
switch (edit.status) {
|
|
||||||
case Context::ChangesManager::Applied:
|
|
||||||
applied++;
|
|
||||||
break;
|
|
||||||
case Context::ChangesManager::Pending:
|
|
||||||
pending++;
|
|
||||||
break;
|
|
||||||
case Context::ChangesManager::Rejected:
|
|
||||||
rejected++;
|
|
||||||
break;
|
|
||||||
case Context::ChangesManager::Archived:
|
|
||||||
total--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool changed = false;
|
|
||||||
if (m_totalEdits != total) {
|
|
||||||
m_totalEdits = total;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_appliedEdits != applied) {
|
|
||||||
m_appliedEdits = applied;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_pendingEdits != pending) {
|
|
||||||
m_pendingEdits = pending;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_rejectedEdits != rejected) {
|
|
||||||
m_rejectedEdits = rejected;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
|
||||||
.arg(total)
|
|
||||||
.arg(applied)
|
|
||||||
.arg(pending)
|
|
||||||
.arg(rejected));
|
|
||||||
emit statsChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class FileEditController : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
void setCurrentRequestId(const QString &requestId);
|
|
||||||
void clearCurrentRequestId();
|
|
||||||
|
|
||||||
int totalEdits() const;
|
|
||||||
int appliedEdits() const;
|
|
||||||
int pendingEdits() const;
|
|
||||||
int rejectedEdits() const;
|
|
||||||
|
|
||||||
void applyFileEdit(const QString &editId);
|
|
||||||
void rejectFileEdit(const QString &editId);
|
|
||||||
void undoFileEdit(const QString &editId);
|
|
||||||
void openFileEditInEditor(const QString &editId);
|
|
||||||
|
|
||||||
void applyAllForCurrentMessage();
|
|
||||||
void undoAllForCurrentMessage();
|
|
||||||
void updateStats();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void statsChanged();
|
|
||||||
void infoMessage(const QString &message);
|
|
||||||
void errorOccurred(const QString &error);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
QString m_currentRequestId;
|
|
||||||
int m_totalEdits{0};
|
|
||||||
int m_appliedEdits{0};
|
|
||||||
int m_pendingEdits{0};
|
|
||||||
int m_rejectedEdits{0};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FileItem.hpp"
|
#include "FileItem.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2026 Petr Mironychev
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FileMentionItem.hpp"
|
#include "FileMentionItem.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2026 Petr Mironychev
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "InputTokenCounter.hpp"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
|
|
||||||
#include <utils/aspects.h>
|
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
|
||||||
#include "ChatModel.hpp"
|
|
||||||
#include "GeneralSettings.hpp"
|
|
||||||
#include "Logger.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
|
||||||
#include "context/ContextManager.hpp"
|
|
||||||
#include "context/TokenUtils.hpp"
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
InputTokenCounter::InputTokenCounter(
|
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, m_chatModel(chatModel)
|
|
||||||
, m_contextManager(contextManager)
|
|
||||||
{
|
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
|
||||||
connect(
|
|
||||||
&settings.useSystemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&InputTokenCounter::recompute);
|
|
||||||
connect(
|
|
||||||
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
|
|
||||||
connect(
|
|
||||||
&settings.enableChatTools,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&InputTokenCounter::recompute);
|
|
||||||
|
|
||||||
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
|
||||||
rewireToolsChangedConnection();
|
|
||||||
recompute();
|
|
||||||
});
|
|
||||||
|
|
||||||
rewireToolsChangedConnection();
|
|
||||||
recompute();
|
|
||||||
}
|
|
||||||
|
|
||||||
int InputTokenCounter::inputTokens() const
|
|
||||||
{
|
|
||||||
return m_inputTokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::setMessage(const QString &message)
|
|
||||||
{
|
|
||||||
m_messageTokens = Context::TokenUtils::estimateTokens(message);
|
|
||||||
recompute();
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::setAttachments(const QStringList &attachments)
|
|
||||||
{
|
|
||||||
m_attachments = attachments;
|
|
||||||
recompute();
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
|
||||||
{
|
|
||||||
m_linkedFiles = linkedFiles;
|
|
||||||
recompute();
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::rewireToolsChangedConnection()
|
|
||||||
{
|
|
||||||
if (m_toolsChangedConn)
|
|
||||||
QObject::disconnect(m_toolsChangedConn);
|
|
||||||
m_toolsChangedConn = {};
|
|
||||||
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
||||||
if (!provider)
|
|
||||||
return;
|
|
||||||
auto *tm = provider->toolsManager();
|
|
||||||
if (!tm)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_toolsChangedConn = connect(
|
|
||||||
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::recompute()
|
|
||||||
{
|
|
||||||
int inputTokens = m_messageTokens;
|
|
||||||
auto &settings = Settings::chatAssistantSettings();
|
|
||||||
|
|
||||||
if (settings.useSystemPrompt()) {
|
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
|
|
||||||
int imageTokens = 0;
|
|
||||||
for (const QString &p : paths) {
|
|
||||||
if (Context::TokenUtils::isImageFilePath(p))
|
|
||||||
imageTokens += Context::TokenUtils::estimateImageAttachmentTokens(p);
|
|
||||||
else
|
|
||||||
textPaths.append(p);
|
|
||||||
}
|
|
||||||
return imageTokens;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!m_attachments.isEmpty()) {
|
|
||||||
QStringList textPaths;
|
|
||||||
inputTokens += splitImageEstimate(m_attachments, textPaths);
|
|
||||||
if (!textPaths.isEmpty()) {
|
|
||||||
auto attachFiles = m_contextManager->getContentFiles(textPaths);
|
|
||||||
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_linkedFiles.isEmpty()) {
|
|
||||||
QStringList textPaths;
|
|
||||||
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
|
|
||||||
if (!textPaths.isEmpty()) {
|
|
||||||
auto linkFiles = m_contextManager->getContentFiles(textPaths);
|
|
||||||
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto &history = m_chatModel->getChatHistory();
|
|
||||||
for (const auto &message : history) {
|
|
||||||
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
|
||||||
inputTokens += 4; // + role
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.enableChatTools()) {
|
|
||||||
const auto providerName = Settings::generalSettings().caProvider();
|
|
||||||
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
|
||||||
providerName)) {
|
|
||||||
if (auto *tm = provider->toolsManager()) {
|
|
||||||
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
|
||||||
if (!toolDefs.isEmpty()) {
|
|
||||||
const QByteArray serialized
|
|
||||||
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
|
||||||
inputTokens += static_cast<int>(serialized.size() / 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
|
||||||
emit inputTokensChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::recordSent()
|
|
||||||
{
|
|
||||||
m_lastSentEstimate = m_calibrationFactor > 0.0
|
|
||||||
? static_cast<int>(m_inputTokens / m_calibrationFactor)
|
|
||||||
: m_inputTokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InputTokenCounter::recordServerUsage(int promptTokens)
|
|
||||||
{
|
|
||||||
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const double rawFactor
|
|
||||||
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
|
|
||||||
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
|
|
||||||
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped;
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
|
|
||||||
.arg(promptTokens)
|
|
||||||
.arg(m_lastSentEstimate)
|
|
||||||
.arg(rawFactor, 0, 'f', 3)
|
|
||||||
.arg(m_calibrationFactor, 0, 'f', 3));
|
|
||||||
|
|
||||||
recompute();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
|
||||||
class ContextManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
class ChatModel;
|
|
||||||
|
|
||||||
class InputTokenCounter : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
InputTokenCounter(
|
|
||||||
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr);
|
|
||||||
|
|
||||||
int inputTokens() const;
|
|
||||||
|
|
||||||
void setMessage(const QString &message);
|
|
||||||
void setAttachments(const QStringList &attachments);
|
|
||||||
void setLinkedFiles(const QStringList &linkedFiles);
|
|
||||||
void recompute();
|
|
||||||
|
|
||||||
void recordSent();
|
|
||||||
void recordServerUsage(int promptTokens);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void inputTokensChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void rewireToolsChangedConnection();
|
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
|
||||||
Context::ContextManager *m_contextManager;
|
|
||||||
QMetaObject::Connection m_toolsChangedConn;
|
|
||||||
|
|
||||||
QStringList m_attachments;
|
|
||||||
QStringList m_linkedFiles;
|
|
||||||
int m_messageTokens{0};
|
|
||||||
int m_inputTokens{0};
|
|
||||||
int m_lastSentEstimate{0};
|
|
||||||
double m_calibrationFactor{1.0};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "SessionFileRegistry.hpp"
|
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <QFileInfo>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
SessionFileRegistry::SessionFileRegistry(QObject *parent)
|
|
||||||
: QObject(parent)
|
|
||||||
{}
|
|
||||||
|
|
||||||
bool SessionFileRegistry::isLocked(const QString &path) const
|
|
||||||
{
|
|
||||||
return !path.isEmpty() && m_lockedPaths.contains(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SessionFileRegistry::lock(const QString &path)
|
|
||||||
{
|
|
||||||
if (path.isEmpty() || m_lockedPaths.contains(path)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
m_lockedPaths.insert(path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SessionFileRegistry::release(const QString &path)
|
|
||||||
{
|
|
||||||
m_lockedPaths.remove(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SessionFileRegistry::setPendingChatFile(const QString &path)
|
|
||||||
{
|
|
||||||
m_pendingChatFile = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SessionFileRegistry::takePendingChatFile()
|
|
||||||
{
|
|
||||||
return std::exchange(m_pendingChatFile, QString{});
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SessionFileRegistry::uniqueFreePath(const QString &desiredPath) const
|
|
||||||
{
|
|
||||||
if (desiredPath.isEmpty() || !m_lockedPaths.contains(desiredPath)) {
|
|
||||||
return desiredPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QFileInfo info(desiredPath);
|
|
||||||
const QString dir = info.path();
|
|
||||||
const QString base = info.completeBaseName();
|
|
||||||
const QString suffix = info.suffix();
|
|
||||||
|
|
||||||
for (int counter = 2;; ++counter) {
|
|
||||||
QString candidate = dir + '/' + base + '_' + QString::number(counter);
|
|
||||||
if (!suffix.isEmpty()) {
|
|
||||||
candidate += '.' + suffix;
|
|
||||||
}
|
|
||||||
if (!m_lockedPaths.contains(candidate)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QSet>
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
|
||||||
|
|
||||||
// Shared registry of chat session (autosave) file paths that are currently held by a live
|
|
||||||
// chat instance. Lets every chat view — bottom pane, navigation panel, editor split — claim
|
|
||||||
// a unique history file so two sessions never autosave into the same path.
|
|
||||||
class SessionFileRegistry : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit SessionFileRegistry(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
bool isLocked(const QString &path) const;
|
|
||||||
bool lock(const QString &path);
|
|
||||||
void release(const QString &path);
|
|
||||||
|
|
||||||
QString uniqueFreePath(const QString &desiredPath) const;
|
|
||||||
|
|
||||||
// Handoff slot for relocating a live chat between hosts (split <-> window): the source
|
|
||||||
// chat stores its history file here, the freshly created host picks it up exactly once.
|
|
||||||
void setPendingChatFile(const QString &path);
|
|
||||||
QString takePendingChatFile();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QSet<QString> m_lockedPaths;
|
|
||||||
QString m_pendingChatFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8Z" fill="black"/>
|
|
||||||
<rect x="11" y="5" width="2" height="9" rx="0.5" fill="black"/>
|
|
||||||
<rect x="7.5" y="8.5" width="9" height="2" rx="0.5" fill="black"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 526 B |
@@ -1,17 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_74_52)">
|
|
||||||
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
|
||||||
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_74_52)">
|
|
||||||
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
|
|
||||||
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
|
|
||||||
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_74_52">
|
|
||||||
<rect width="44" height="44" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 943 B |
@@ -1,6 +1,17 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round">
|
<g clip-path="url(#clip0_74_52)">
|
||||||
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="black"/>
|
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||||
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="none"/>
|
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_74_52)">
|
||||||
|
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
|
||||||
|
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
|
||||||
</g>
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_74_52">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 943 B |
@@ -1,6 +0,0 @@
|
|||||||
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="translate(10 8) skewX(-15)" stroke="black" stroke-width="2" stroke-linejoin="round">
|
|
||||||
<rect x="10" y="0" width="22" height="15" rx="3" ry="3" fill="none"/>
|
|
||||||
<rect x="0" y="12" width="22" height="15" rx="3" ry="3" fill="black"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 348 B |
@@ -1,5 +0,0 @@
|
|||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 3L22 20H2L12 3Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
|
|
||||||
<path d="M12 10V14" stroke="black" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
<path d="M12 17H12.01" stroke="black" stroke-width="2.4" stroke-linecap="round"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 350 B |
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -20,9 +19,6 @@ ChatRootView {
|
|||||||
colorGroup: SystemPalette.Active
|
colorGroup: SystemPalette.Active
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool hasActiveError: false
|
|
||||||
readonly property color errorColor: "#d32f2f"
|
|
||||||
|
|
||||||
palette {
|
palette {
|
||||||
window: sysPalette.window
|
window: sysPalette.window
|
||||||
windowText: sysPalette.windowText
|
windowText: sysPalette.windowText
|
||||||
@@ -91,28 +87,11 @@ ChatRootView {
|
|||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: childrenRect.height + 10
|
Layout.preferredHeight: childrenRect.height + 10
|
||||||
|
|
||||||
isInEditor: root.isInEditor
|
|
||||||
saveButton.onClicked: root.showSaveDialog()
|
saveButton.onClicked: root.showSaveDialog()
|
||||||
loadButton.onClicked: root.showLoadDialog()
|
loadButton.onClicked: root.showLoadDialog()
|
||||||
clearButton.onClicked: root.clearChat()
|
clearButton.onClicked: root.clearChat()
|
||||||
newChatButton.onClicked: root.requestNewChat()
|
|
||||||
tokensBadge {
|
tokensBadge {
|
||||||
readonly property int sessionPrompt: root.chatModel.sessionPromptTokens || 0
|
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||||
readonly property int sessionCompletion: root.chatModel.sessionCompletionTokens || 0
|
|
||||||
readonly property int sessionCached: root.chatModel.sessionCachedPromptTokens || 0
|
|
||||||
text: sessionCached > 0
|
|
||||||
? qsTr("next ~%1 · session ↑%2 ↓%3 ↻%4")
|
|
||||||
.arg(root.inputTokensCount)
|
|
||||||
.arg(sessionPrompt)
|
|
||||||
.arg(sessionCompletion)
|
|
||||||
.arg(sessionCached)
|
|
||||||
: qsTr("next ~%1 · session ↑%2 ↓%3")
|
|
||||||
.arg(root.inputTokensCount)
|
|
||||||
.arg(sessionPrompt)
|
|
||||||
.arg(sessionCompletion)
|
|
||||||
ToolTip.text: sessionCached > 0
|
|
||||||
? qsTr("next request (estimate) · session prompt ↑ / completion ↓ / cached ↻ (provider cache hits)")
|
|
||||||
: qsTr("next request (estimate) · session prompt ↑ / completion ↓")
|
|
||||||
}
|
}
|
||||||
recentPath {
|
recentPath {
|
||||||
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||||
@@ -124,20 +103,6 @@ ChatRootView {
|
|||||||
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||||
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
||||||
}
|
}
|
||||||
relocateButton {
|
|
||||||
icon.source: (typeof _chatview !== 'undefined')
|
|
||||||
? "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
|
||||||
: "qrc:/qt/qml/ChatView/icons/open-in-window.svg"
|
|
||||||
onClicked: {
|
|
||||||
if (typeof _chatview !== 'undefined')
|
|
||||||
root.relocateToSplit()
|
|
||||||
else
|
|
||||||
root.relocateToWindow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
relocateTooltip.text: (typeof _chatview !== 'undefined')
|
|
||||||
? qsTr("Move this chat to an editor tab")
|
|
||||||
: qsTr("Move this chat to a separate window")
|
|
||||||
toolsButton {
|
toolsButton {
|
||||||
checked: root.useTools
|
checked: root.useTools
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
@@ -179,48 +144,20 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
MessageNavigator {
|
|
||||||
id: messageNavigator
|
|
||||||
|
|
||||||
Layout.preferredWidth: 16
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.topMargin: 4
|
|
||||||
Layout.bottomMargin: 4
|
|
||||||
|
|
||||||
chatModel: root.chatModel
|
|
||||||
|
|
||||||
onMessageClicked: function(messageIndex) {
|
|
||||||
chatListView.userScrolledUp = true
|
|
||||||
chatListView.positionViewAtIndex(messageIndex, ListView.Beginning)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: chatListView
|
id: chatListView
|
||||||
|
|
||||||
property bool userScrolledUp: false
|
property bool userScrolledUp: false
|
||||||
|
|
||||||
function syncNavigatorCurrent() {
|
|
||||||
const top = indexAt(10, contentY + 4)
|
|
||||||
messageNavigator.updateCurrentFromModelIndex(top)
|
|
||||||
}
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
leftMargin: 3
|
leftMargin: 5
|
||||||
model: root.chatModel
|
model: root.chatModel
|
||||||
clip: true
|
clip: true
|
||||||
spacing: 0
|
spacing: 0
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
cacheBuffer: 2000
|
cacheBuffer: 2000
|
||||||
|
|
||||||
onContentYChanged: Qt.callLater(syncNavigatorCurrent)
|
|
||||||
|
|
||||||
onMovingChanged: {
|
onMovingChanged: {
|
||||||
if (moving) {
|
if (moving) {
|
||||||
userScrolledUp = !atYEnd
|
userScrolledUp = !atYEnd
|
||||||
@@ -307,7 +244,6 @@ ChatRootView {
|
|||||||
if (!userScrolledUp) {
|
if (!userScrolledUp) {
|
||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
Qt.callLater(syncNavigatorCurrent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentHeightChanged: {
|
onContentHeightChanged: {
|
||||||
@@ -323,7 +259,6 @@ ChatRootView {
|
|||||||
id: chatItemInstance
|
id: chatItemInstance
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
chatViewport: chatListView
|
|
||||||
msgModel: root.chatModel.processMessageContent(model.content)
|
msgModel: root.chatModel.processMessageContent(model.content)
|
||||||
messageAttachments: model.attachments
|
messageAttachments: model.attachments
|
||||||
messageImages: model.images
|
messageImages: model.images
|
||||||
@@ -335,10 +270,6 @@ ChatRootView {
|
|||||||
codeFontSize: root.codeFontSize
|
codeFontSize: root.codeFontSize
|
||||||
textFontSize: root.textFontSize
|
textFontSize: root.textFontSize
|
||||||
textFormat: root.textFormat
|
textFormat: root.textFormat
|
||||||
promptTokens: model.promptTokens || 0
|
|
||||||
completionTokens: model.completionTokens || 0
|
|
||||||
cachedPromptTokens: model.cachedPromptTokens || 0
|
|
||||||
reasoningTokens: model.reasoningTokens || 0
|
|
||||||
|
|
||||||
onResetChatToMessage: function(idx) {
|
onResetChatToMessage: function(idx) {
|
||||||
messageInput.text = model.content
|
messageInput.text = model.content
|
||||||
@@ -403,7 +334,6 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
id: view
|
id: view
|
||||||
@@ -415,10 +345,11 @@ ChatRootView {
|
|||||||
QQC.TextArea {
|
QQC.TextArea {
|
||||||
id: messageInput
|
id: messageInput
|
||||||
|
|
||||||
placeholderText: qsTr("Type your message here... (%1 to send)").arg(root.sendShortcutText)
|
placeholderText: Qt.platform.os === "osx"
|
||||||
|
? qsTr("Type your message here... (⌘+↩ to send)")
|
||||||
|
: qsTr("Type your message here... (Ctrl+Enter to send)")
|
||||||
placeholderTextColor: palette.mid
|
placeholderTextColor: palette.mid
|
||||||
color: palette.text
|
color: palette.text
|
||||||
wrapMode: TextArea.Wrap
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
radius: 2
|
radius: 2
|
||||||
color: palette.base
|
color: palette.base
|
||||||
@@ -441,31 +372,15 @@ ChatRootView {
|
|||||||
root.calculateMessageTokensCount(messageInput.text)
|
root.calculateMessageTokensCount(messageInput.text)
|
||||||
var cursorPos = messageInput.cursorPosition
|
var cursorPos = messageInput.cursorPosition
|
||||||
var textBefore = messageInput.text.substring(0, cursorPos)
|
var textBefore = messageInput.text.substring(0, cursorPos)
|
||||||
|
|
||||||
var atIndex = textBefore.lastIndexOf('@')
|
var atIndex = textBefore.lastIndexOf('@')
|
||||||
if (atIndex >= 0) {
|
if (atIndex >= 0) {
|
||||||
var query = textBefore.substring(atIndex + 1)
|
var query = textBefore.substring(atIndex + 1)
|
||||||
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
||||||
fileMentionPopup.updateSearch(query)
|
fileMentionPopup.updateSearch(query)
|
||||||
skillCommandPopup.dismiss()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileMentionPopup.dismiss()
|
fileMentionPopup.dismiss()
|
||||||
|
|
||||||
const slashIndex = textBefore.lastIndexOf('/')
|
|
||||||
if (slashIndex >= 0) {
|
|
||||||
const beforeSlash = slashIndex === 0
|
|
||||||
? ' '
|
|
||||||
: textBefore.charAt(slashIndex - 1)
|
|
||||||
const skillQuery = textBefore.substring(slashIndex + 1)
|
|
||||||
if ((beforeSlash === ' ' || beforeSlash === '\n')
|
|
||||||
&& /^[a-z0-9-]*$/.test(skillQuery)) {
|
|
||||||
skillCommandPopup.updateSearch(skillQuery)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
skillCommandPopup.dismiss()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
@@ -483,23 +398,6 @@ ChatRootView {
|
|||||||
fileMentionPopup.dismiss()
|
fileMentionPopup.dismiss()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
}
|
||||||
} else if (skillCommandPopup.visible) {
|
|
||||||
if (event.key === Qt.Key_Down) {
|
|
||||||
skillCommandPopup.moveDown()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
skillCommandPopup.moveUp()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
||||||
root.applySkillSelection()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Escape) {
|
|
||||||
skillCommandPopup.dismiss()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
} else if (root.isSendShortcut(event.key, event.modifiers)) {
|
|
||||||
root.sendChatMessage()
|
|
||||||
event.accepted = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,21 +490,13 @@ ChatRootView {
|
|||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: 40
|
||||||
|
|
||||||
isCompressing: root.isCompressing
|
isCompressing: root.isCompressing
|
||||||
isProcessing: root.isRequestInProgress
|
|
||||||
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||||
: root.cancelRequest()
|
: root.cancelRequest()
|
||||||
sendButton.icon.source: root.isRequestInProgress
|
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
||||||
? ""
|
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||||
: (root.hasActiveError ? "qrc:/qt/qml/ChatView/icons/warning-icon.svg"
|
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
|
||||||
: "qrc:/qt/qml/ChatView/icons/chat-icon.svg")
|
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
||||||
sendButton.text: root.isRequestInProgress ? qsTr("Stop") : qsTr("Send")
|
: qsTr("Stop")
|
||||||
sendButton.accentColor: (root.hasActiveError && !root.isRequestInProgress)
|
|
||||||
? root.errorColor : "transparent"
|
|
||||||
sendButtonTooltip.text: root.isRequestInProgress
|
|
||||||
? qsTr("Stop")
|
|
||||||
: (root.hasActiveError
|
|
||||||
? root.lastErrorMessage
|
|
||||||
: qsTr("Send message to LLM %1").arg(root.sendShortcutText))
|
|
||||||
compressButton.onClicked: compressConfirmDialog.open()
|
compressButton.onClicked: compressConfirmDialog.open()
|
||||||
cancelCompressButton.onClicked: root.cancelCompression()
|
cancelCompressButton.onClicked: root.cancelCompression()
|
||||||
syncOpenFiles {
|
syncOpenFiles {
|
||||||
@@ -619,6 +509,15 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
id: sendMessageShortcut
|
||||||
|
|
||||||
|
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||||
|
context: Qt.WindowShortcut
|
||||||
|
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
|
||||||
|
onActivated: root.sendChatMessage()
|
||||||
|
}
|
||||||
|
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.clearMessages()
|
root.clearMessages()
|
||||||
root.clearAttachmentFiles()
|
root.clearAttachmentFiles()
|
||||||
@@ -629,31 +528,6 @@ ChatRootView {
|
|||||||
Qt.callLater(chatListView.positionViewAtEnd)
|
Qt.callLater(chatListView.positionViewAtEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusInput() {
|
|
||||||
messageInput.forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
property Item focusGuard: Window.activeFocusItem
|
|
||||||
onFocusGuardChanged: Qt.callLater(returnFocusToInputIfNeeded)
|
|
||||||
|
|
||||||
function returnFocusToInputIfNeeded() {
|
|
||||||
var item = Window.activeFocusItem
|
|
||||||
if (!item || item === messageInput)
|
|
||||||
return
|
|
||||||
if (item.cursorVisible !== undefined || item.selectByMouse !== undefined)
|
|
||||||
return
|
|
||||||
if (item.popup !== undefined)
|
|
||||||
return
|
|
||||||
var p = item
|
|
||||||
while (p) {
|
|
||||||
if (p === root) {
|
|
||||||
messageInput.forceActiveFocus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMentionSelection() {
|
function applyMentionSelection() {
|
||||||
var result = fileMentionPopup.applyCurrentSelection(
|
var result = fileMentionPopup.applyCurrentSelection(
|
||||||
messageInput.text, messageInput.cursorPosition, root.useTools)
|
messageInput.text, messageInput.cursorPosition, root.useTools)
|
||||||
@@ -663,25 +537,7 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySkillSelection() {
|
|
||||||
const name = skillCommandPopup.currentName()
|
|
||||||
if (name === "")
|
|
||||||
return
|
|
||||||
const cursorPos = messageInput.cursorPosition
|
|
||||||
const textBefore = messageInput.text.substring(0, cursorPos)
|
|
||||||
const slashIndex = textBefore.lastIndexOf('/')
|
|
||||||
if (slashIndex < 0)
|
|
||||||
return
|
|
||||||
const before = messageInput.text.substring(0, slashIndex)
|
|
||||||
const after = messageInput.text.substring(cursorPos)
|
|
||||||
const token = '/' + name + ' '
|
|
||||||
messageInput.text = before + token + after
|
|
||||||
messageInput.cursorPosition = before.length + token.length
|
|
||||||
skillCommandPopup.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.hasActiveError = false
|
|
||||||
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
fileMentionPopup.clearMentions()
|
fileMentionPopup.clearMentions()
|
||||||
@@ -704,122 +560,13 @@ ChatRootView {
|
|||||||
onAccepted: root.compressCurrentChat()
|
onAccepted: root.compressCurrentChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Toast {
|
||||||
id: errorBanner
|
id: errorToast
|
||||||
|
|
||||||
z: 1000
|
z: 1000
|
||||||
visible: root.hasActiveError && root.lastErrorMessage.length > 0
|
|
||||||
|
|
||||||
width: parent.width / 2
|
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
|
||||||
anchors.right: parent.right
|
border.color: Qt.darker(infoToast.color, 1.3)
|
||||||
anchors.bottom: parent.bottom
|
toastTextColor: "#FFFFFF"
|
||||||
anchors.rightMargin: 10
|
|
||||||
anchors.bottomMargin: bottomBar.height + 48
|
|
||||||
|
|
||||||
height: visible ? errorRow.implicitHeight + 12 : 0
|
|
||||||
|
|
||||||
color: Qt.rgba(0.83, 0.18, 0.18, 0.96)
|
|
||||||
radius: 6
|
|
||||||
border.color: Qt.darker(color, 1.3)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: errorRow
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
anchors.rightMargin: 6
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: root.lastErrorMessage
|
|
||||||
color: "#FFFFFF"
|
|
||||||
font.pixelSize: 12
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
selectionColor: Qt.darker(errorBanner.color, 1.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: copyErrorButton
|
|
||||||
|
|
||||||
property bool copied: false
|
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
implicitWidth: copyErrorLabel.implicitWidth + 18
|
|
||||||
implicitHeight: 22
|
|
||||||
radius: 4
|
|
||||||
color: copyErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28)
|
|
||||||
: Qt.rgba(1, 1, 1, 0.16)
|
|
||||||
border.color: Qt.rgba(1, 1, 1, 0.45)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: copyErrorLabel
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: copyErrorButton.copied ? qsTr("Copied") : qsTr("Copy")
|
|
||||||
color: "#FFFFFF"
|
|
||||||
font.pixelSize: 12
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: copyErrorMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
root.copyToClipboard(root.lastErrorMessage)
|
|
||||||
copyErrorButton.copied = true
|
|
||||||
copyErrorResetTimer.restart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: copyErrorResetTimer
|
|
||||||
|
|
||||||
interval: 1500
|
|
||||||
onTriggered: copyErrorButton.copied = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: closeErrorButton
|
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
implicitWidth: 22
|
|
||||||
implicitHeight: 22
|
|
||||||
radius: 4
|
|
||||||
color: closeErrorMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.28) : "transparent"
|
|
||||||
border.color: Qt.rgba(1, 1, 1, 0.45)
|
|
||||||
border.width: closeErrorMouse.containsMouse ? 1 : 0
|
|
||||||
|
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "✕"
|
|
||||||
color: "#FFFFFF"
|
|
||||||
font.pixelSize: 12
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: closeErrorMouse
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.hasActiveError = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast {
|
Toast {
|
||||||
@@ -859,7 +606,7 @@ ChatRootView {
|
|||||||
target: root
|
target: root
|
||||||
function onLastErrorMessageChanged() {
|
function onLastErrorMessageChanged() {
|
||||||
if (root.lastErrorMessage.length > 0) {
|
if (root.lastErrorMessage.length > 0) {
|
||||||
root.hasActiveError = true
|
errorToast.show(root.lastErrorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onLastInfoMessageChanged() {
|
function onLastInfoMessageChanged() {
|
||||||
@@ -889,21 +636,7 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SkillCommandPopup {
|
|
||||||
id: skillCommandPopup
|
|
||||||
|
|
||||||
z: 999
|
|
||||||
width: Math.min(480, root.width - 20)
|
|
||||||
|
|
||||||
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
|
|
||||||
y: view.y - height - 4
|
|
||||||
|
|
||||||
skillProvider: root
|
|
||||||
|
|
||||||
onSelectionRequested: root.applySkillSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
focusInput()
|
messageInput.forceActiveFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ChatView
|
import ChatView
|
||||||
@@ -31,16 +30,10 @@ Rectangle {
|
|||||||
property int textFontSize: Qt.application.font.pointSize
|
property int textFontSize: Qt.application.font.pointSize
|
||||||
property int codeFontSize: Qt.application.font.pointSize
|
property int codeFontSize: Qt.application.font.pointSize
|
||||||
property int textFormat: 0
|
property int textFormat: 0
|
||||||
property Flickable chatViewport: null
|
|
||||||
|
|
||||||
property bool isUserMessage: false
|
property bool isUserMessage: false
|
||||||
property int messageIndex: -1
|
property int messageIndex: -1
|
||||||
|
|
||||||
property int promptTokens: 0
|
|
||||||
property int completionTokens: 0
|
|
||||||
property int cachedPromptTokens: 0
|
|
||||||
property int reasoningTokens: 0
|
|
||||||
|
|
||||||
signal resetChatToMessage(int index)
|
signal resetChatToMessage(int index)
|
||||||
signal openFileRequested(string filePath)
|
signal openFileRequested(string filePath)
|
||||||
|
|
||||||
@@ -142,39 +135,6 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: usageBadge
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 10
|
|
||||||
Layout.rightMargin: 10
|
|
||||||
spacing: 8
|
|
||||||
visible: !root.isUserMessage
|
|
||||||
&& (root.promptTokens > 0 || root.completionTokens > 0)
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.cachedPromptTokens > 0
|
|
||||||
? qsTr("↑ %1 (cached %2)").arg(root.promptTokens).arg(root.cachedPromptTokens)
|
|
||||||
: qsTr("↑ %1").arg(root.promptTokens)
|
|
||||||
color: palette.placeholderText
|
|
||||||
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: root.reasoningTokens > 0
|
|
||||||
? qsTr("↓ %1 (reasoning %2)").arg(root.completionTokens).arg(root.reasoningTokens)
|
|
||||||
: qsTr("↓ %1").arg(root.completionTokens)
|
|
||||||
color: palette.placeholderText
|
|
||||||
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
|
||||||
}
|
|
||||||
Text {
|
|
||||||
text: qsTr("Σ %1").arg(root.promptTokens + root.completionTokens)
|
|
||||||
color: palette.placeholderText
|
|
||||||
font.pointSize: Math.max(root.textFontSize - 2, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -261,7 +221,6 @@ Rectangle {
|
|||||||
language: itemData.language
|
language: itemData.language
|
||||||
codeFontFamily: root.codeFontFamily
|
codeFontFamily: root.codeFontFamily
|
||||||
codeFontSize: root.codeFontSize
|
codeFontSize: root.codeFontSize
|
||||||
viewport: root.chatViewport
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component AttachmentComponent : Rectangle {
|
component AttachmentComponent : Rectangle {
|
||||||
@@ -356,9 +315,10 @@ Rectangle {
|
|||||||
smooth: true
|
smooth: true
|
||||||
mipmap: true
|
mipmap: true
|
||||||
|
|
||||||
QoABusyIndicator {
|
BusyIndicator {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
running: imageDisplay.status === Image.Loading
|
running: imageDisplay.status === Image.Loading
|
||||||
|
visible: running
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -14,7 +13,6 @@ Rectangle {
|
|||||||
property string code: ""
|
property string code: ""
|
||||||
property string language: ""
|
property string language: ""
|
||||||
property bool expanded: false
|
property bool expanded: false
|
||||||
property Flickable viewport: null
|
|
||||||
|
|
||||||
property alias codeFontFamily: codeText.font.family
|
property alias codeFontFamily: codeText.font.family
|
||||||
property alias codeFontSize: codeText.font.pointSize
|
property alias codeFontSize: codeText.font.pointSize
|
||||||
@@ -124,16 +122,7 @@ Rectangle {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
|
|
||||||
y: {
|
y: 5
|
||||||
if (!root.expanded || !root.viewport)
|
|
||||||
return 5
|
|
||||||
const flick = root.viewport
|
|
||||||
const topInContent = root.mapToItem(flick.contentItem, 0, 0).y
|
|
||||||
const topInView = topInContent - flick.contentY
|
|
||||||
const desired = topInView < 0 ? (-topInView + 5) : 5
|
|
||||||
const maxY = Math.max(5, root.height - copyButton.height - 5)
|
|
||||||
return Math.max(5, Math.min(desired, maxY))
|
|
||||||
}
|
|
||||||
text: qsTr("Copy")
|
text: qsTr("Copy")
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -194,24 +193,9 @@ Rectangle {
|
|||||||
color: root.statusColor
|
color: root.statusColor
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/open-in-code.svg"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: root.openInEditor(editData.edit_id)
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: parent.hovered
|
|
||||||
delay: 500
|
|
||||||
text: qsTr("Open file in editor and navigate to changes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: headerText
|
id: headerText
|
||||||
|
Layout.fillWidth: true
|
||||||
text: {
|
text: {
|
||||||
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
||||||
if (root.oldContent.length > 0) {
|
if (root.oldContent.length > 0) {
|
||||||
@@ -239,19 +223,6 @@ Rectangle {
|
|||||||
color: palette.mid
|
color: palette.mid
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: actionButtons
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 5
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: !root.isPending
|
visible: !root.isPending
|
||||||
Layout.preferredWidth: badgeText.width + 12
|
Layout.preferredWidth: badgeText.width + 12
|
||||||
@@ -268,6 +239,31 @@ Rectangle {
|
|||||||
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: actionButtons
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: 5
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: root.openInEditor(editData.edit_id)
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open file in editor and navigate to changes")
|
||||||
|
ToolTip.delay: 500
|
||||||
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
icon {
|
icon {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.platform as Platform
|
import Qt.labs.platform as Platform
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Qt.labs.platform as Platform
|
|
||||||
import ChatView
|
import ChatView
|
||||||
import UIControls
|
import UIControls
|
||||||
|
|
||||||
@@ -54,7 +52,7 @@ Flow {
|
|||||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||||
onClicked: (mouse) => {
|
onClicked: (mouse) => {
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
contextMenu.open()
|
contextMenu.popup()
|
||||||
} else if (mouse.button === Qt.MiddleButton ||
|
} else if (mouse.button === Qt.MiddleButton ||
|
||||||
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
|
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
|
||||||
root.removeFileFromListByIndex(fileItem.index)
|
root.removeFileFromListByIndex(fileItem.index)
|
||||||
@@ -72,23 +70,23 @@ Flow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.Menu {
|
Menu {
|
||||||
id: contextMenu
|
id: contextMenu
|
||||||
|
|
||||||
Platform.MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Open in Qt Creator")
|
text: "Open in Qt Creator"
|
||||||
onTriggered: fileItem.openFileInEditor()
|
onTriggered: fileItem.openFileInEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Open in External Editor")
|
text: "Open in External Editor"
|
||||||
onTriggered: fileItem.openFileInExternalEditor()
|
onTriggered: fileItem.openFileInExternalEditor()
|
||||||
}
|
}
|
||||||
|
|
||||||
Platform.MenuSeparator {}
|
MenuSeparator {}
|
||||||
|
|
||||||
Platform.MenuItem {
|
MenuItem {
|
||||||
text: qsTr("Remove")
|
text: "Remove"
|
||||||
onTriggered: root.removeFileFromListByIndex(fileItem.index)
|
onTriggered: root.removeFileFromListByIndex(fileItem.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -20,8 +19,6 @@ Rectangle {
|
|||||||
property alias cancelCompressButton: cancelCompressButtonId
|
property alias cancelCompressButton: cancelCompressButtonId
|
||||||
|
|
||||||
property bool isCompressing: false
|
property bool isCompressing: false
|
||||||
property bool isProcessing: false
|
|
||||||
property alias sendButtonTooltip: sendButtonTooltipId
|
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
@@ -48,12 +45,9 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: attachFilesId.hovered
|
ToolTip.text: qsTr("Attach file to message")
|
||||||
delay: 250
|
|
||||||
text: qsTr("Attach file to message")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -64,12 +58,9 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: attachImagesId.hovered
|
ToolTip.text: qsTr("Attach image to message")
|
||||||
delay: 250
|
|
||||||
text: qsTr("Attach image to message")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -80,12 +71,9 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: linkFilesId.hovered
|
ToolTip.text: qsTr("Link file to context")
|
||||||
delay: 250
|
|
||||||
text: qsTr("Link file to context")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
@@ -93,10 +81,8 @@ Rectangle {
|
|||||||
|
|
||||||
text: qsTr("Sync open files")
|
text: qsTr("Sync open files")
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: syncOpenFilesId.hovered
|
||||||
visible: syncOpenFilesId.hovered
|
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||||
text: qsTr("Automatically synchronize currently opened files with the model context")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -109,7 +95,7 @@ Rectangle {
|
|||||||
visible: root.isCompressing
|
visible: root.isCompressing
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
QoABusyIndicator {
|
BusyIndicator {
|
||||||
id: compressBusyIndicator
|
id: compressBusyIndicator
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -131,11 +117,9 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: qsTr("Cancel")
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: cancelCompressButtonId.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: qsTr("Cancel compression")
|
||||||
text: qsTr("Cancel compression")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,41 +134,20 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: compressButtonId.hovered
|
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||||
delay: 250
|
|
||||||
text: qsTr("Compress chat (create summarized copy using LLM)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: sendButtonId
|
id: sendButtonId
|
||||||
|
|
||||||
leftPadding: root.isProcessing ? 22 : 4
|
|
||||||
|
|
||||||
icon {
|
icon {
|
||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoABusyIndicator {
|
ToolTip.delay: 250
|
||||||
id: sendBusyIndicator
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 5
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 14
|
|
||||||
height: 14
|
|
||||||
running: root.isProcessing
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
id: sendButtonTooltipId
|
|
||||||
|
|
||||||
visible: sendButtonId.hovered
|
|
||||||
delay: 250
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -119,13 +118,11 @@ Rectangle {
|
|||||||
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
|
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
|
||||||
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
|
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: applyAllButton.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: root.hasPendingEdits
|
||||||
text: root.hasPendingEdits
|
|
||||||
? qsTr("Apply all pending and rejected edits in this message")
|
? qsTr("Apply all pending and rejected edits in this message")
|
||||||
: qsTr("Reapply all rejected edits in this message")
|
: qsTr("Reapply all rejected edits in this message")
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: root.applyAllClicked()
|
onClicked: root.applyAllClicked()
|
||||||
}
|
}
|
||||||
@@ -137,11 +134,9 @@ Rectangle {
|
|||||||
enabled: root.hasAppliedEdits
|
enabled: root.hasAppliedEdits
|
||||||
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
|
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: undoAllButton.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: qsTr("Undo all applied edits in this message")
|
||||||
text: qsTr("Undo all applied edits in this message")
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: root.undoAllClicked()
|
onClicked: root.undoAllClicked()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2026 Petr Mironychev
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import ChatView
|
|
||||||
import UIControls
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: nav
|
|
||||||
|
|
||||||
property var chatModel
|
|
||||||
property var entries: []
|
|
||||||
property color dotColor: "#92BD6C"
|
|
||||||
property int currentMessageIndex: -1
|
|
||||||
|
|
||||||
readonly property int dotCount: entries.length
|
|
||||||
readonly property int verticalPadding: 8
|
|
||||||
readonly property int minDotSpacing: 18
|
|
||||||
readonly property real availableHeight: Math.max(0, height - 2 * verticalPadding)
|
|
||||||
readonly property real naturalHeight: dotCount > 1 ? (dotCount - 1) * minDotSpacing : 0
|
|
||||||
readonly property bool needsScrolling: naturalHeight > availableHeight
|
|
||||||
readonly property real contentHeight: needsScrolling
|
|
||||||
? naturalHeight + 2 * verticalPadding
|
|
||||||
: Math.max(height, 2 * verticalPadding)
|
|
||||||
|
|
||||||
signal messageClicked(int messageIndex)
|
|
||||||
|
|
||||||
implicitWidth: 16
|
|
||||||
|
|
||||||
function rebuild() {
|
|
||||||
entries = chatModel ? chatModel.userMessagePreviews(80) : []
|
|
||||||
Qt.callLater(scrollCurrentIntoView)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCurrentFromModelIndex(modelIdx) {
|
|
||||||
if (modelIdx < 0) {
|
|
||||||
currentMessageIndex = -1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let best = -1
|
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
|
||||||
const e = entries[i]
|
|
||||||
if (!e)
|
|
||||||
continue
|
|
||||||
const mi = e.messageIndex
|
|
||||||
if (mi <= modelIdx)
|
|
||||||
best = mi
|
|
||||||
else
|
|
||||||
break
|
|
||||||
}
|
|
||||||
currentMessageIndex = best
|
|
||||||
}
|
|
||||||
|
|
||||||
function uiIndexOf(messageIndex) {
|
|
||||||
for (let i = 0; i < entries.length; ++i) {
|
|
||||||
const e = entries[i]
|
|
||||||
if (e && e.messageIndex === messageIndex)
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function dotCenterY(uiIndex) {
|
|
||||||
const count = dotCount
|
|
||||||
if (count <= 1)
|
|
||||||
return contentHeight / 2
|
|
||||||
const spacing = needsScrolling
|
|
||||||
? minDotSpacing
|
|
||||||
: availableHeight / (count - 1)
|
|
||||||
return verticalPadding + spacing * uiIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollCurrentIntoView() {
|
|
||||||
if (!needsScrolling || currentMessageIndex < 0)
|
|
||||||
return
|
|
||||||
const ui = uiIndexOf(currentMessageIndex)
|
|
||||||
if (ui < 0)
|
|
||||||
return
|
|
||||||
const y = dotCenterY(ui)
|
|
||||||
const margin = 24
|
|
||||||
if (y < flick.contentY + margin)
|
|
||||||
flick.contentY = Math.max(0, y - margin)
|
|
||||||
else if (y > flick.contentY + flick.height - margin)
|
|
||||||
flick.contentY = Math.min(
|
|
||||||
Math.max(0, flick.contentHeight - flick.height),
|
|
||||||
y - flick.height + margin)
|
|
||||||
}
|
|
||||||
|
|
||||||
onChatModelChanged: rebuild()
|
|
||||||
onCurrentMessageIndexChanged: scrollCurrentIntoView()
|
|
||||||
Component.onCompleted: rebuild()
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: nav.chatModel
|
|
||||||
ignoreUnknownSignals: true
|
|
||||||
function onRowsInserted() { nav.rebuild() }
|
|
||||||
function onRowsRemoved() { nav.rebuild() }
|
|
||||||
function onModelReset() { nav.rebuild() }
|
|
||||||
function onModelReseted() { nav.rebuild() }
|
|
||||||
function onDataChanged() { nav.rebuild() }
|
|
||||||
}
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
id: flick
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: nav.contentHeight
|
|
||||||
interactive: nav.needsScrolling
|
|
||||||
clip: true
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: spine
|
|
||||||
|
|
||||||
visible: nav.dotCount > 1
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
y: nav.verticalPadding
|
|
||||||
width: 1
|
|
||||||
height: Math.max(0, flick.contentHeight - 2 * nav.verticalPadding)
|
|
||||||
color: palette.mid
|
|
||||||
opacity: 0.4
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: nav.entries
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: dotItem
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
readonly property int msgIndex: modelData && modelData.messageIndex !== undefined
|
|
||||||
? modelData.messageIndex : -1
|
|
||||||
readonly property string preview: modelData && modelData.preview !== undefined
|
|
||||||
? modelData.preview : ""
|
|
||||||
readonly property bool isCurrent: nav.currentMessageIndex === msgIndex
|
|
||||||
|
|
||||||
width: 16
|
|
||||||
height: 14
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
y: nav.dotCenterY(index) - height / 2
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: dot
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: dotItem.isCurrent ? 11 : (dotArea.containsMouse ? 10 : 7)
|
|
||||||
height: width
|
|
||||||
radius: width / 2
|
|
||||||
color: dotArea.containsMouse
|
|
||||||
? Qt.lighter(nav.dotColor, 1.2)
|
|
||||||
: nav.dotColor
|
|
||||||
border.color: dotItem.isCurrent
|
|
||||||
? Qt.darker(nav.dotColor, 1.7)
|
|
||||||
: Qt.darker(nav.dotColor, 1.4)
|
|
||||||
border.width: dotItem.isCurrent ? 2 : 1
|
|
||||||
opacity: dotItem.isCurrent || dotArea.containsMouse ? 1.0 : 0.55
|
|
||||||
|
|
||||||
Behavior on width { NumberAnimation { duration: 120 } }
|
|
||||||
Behavior on opacity { NumberAnimation { duration: 120 } }
|
|
||||||
Behavior on color { ColorAnimation { duration: 120 } }
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: dotArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: nav.messageClicked(dotItem.msgIndex)
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: dotArea.containsMouse
|
|
||||||
delay: 350
|
|
||||||
text: dotItem.preview.length > 0
|
|
||||||
? qsTr("#%1 · %2").arg(dotItem.index + 1).arg(dotItem.preview)
|
|
||||||
: qsTr("Jump to message #%1").arg(dotItem.index + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
// Copyright (C) 2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Layouts
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// Object exposing Q_INVOKABLE QVariantList searchSkills(query).
|
|
||||||
property var skillProvider: null
|
|
||||||
property var searchResults: []
|
|
||||||
property int currentIndex: 0
|
|
||||||
|
|
||||||
signal selectionRequested()
|
|
||||||
|
|
||||||
visible: searchResults.length > 0
|
|
||||||
height: Math.min(searchResults.length * 40, 40 * 6) + 2
|
|
||||||
|
|
||||||
color: palette.window
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
radius: 4
|
|
||||||
|
|
||||||
function updateSearch(query) {
|
|
||||||
searchResults = skillProvider ? skillProvider.searchSkills(query) : []
|
|
||||||
currentIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismiss() {
|
|
||||||
searchResults = []
|
|
||||||
currentIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveUp() {
|
|
||||||
if (currentIndex > 0)
|
|
||||||
currentIndex--
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveDown() {
|
|
||||||
if (currentIndex < searchResults.length - 1)
|
|
||||||
currentIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentName() {
|
|
||||||
if (currentIndex >= 0 && currentIndex < searchResults.length)
|
|
||||||
return searchResults[currentIndex].name
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentIndexChanged: listView.positionViewAtIndex(currentIndex, ListView.Contain)
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: listView
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 1
|
|
||||||
model: root.searchResults
|
|
||||||
currentIndex: root.currentIndex
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
id: delegateItem
|
|
||||||
|
|
||||||
required property int index
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
width: listView.width
|
|
||||||
height: 40
|
|
||||||
color: index === root.currentIndex
|
|
||||||
? palette.highlight
|
|
||||||
: (hoverArea.containsMouse
|
|
||||||
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
|
|
||||||
: "transparent")
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
anchors.rightMargin: 10
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.bottomMargin: 4
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: "/" + delegateItem.modelData.name
|
|
||||||
color: delegateItem.index === root.currentIndex
|
|
||||||
? palette.highlightedText
|
|
||||||
: palette.text
|
|
||||||
font.bold: true
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: delegateItem.modelData.description
|
|
||||||
color: delegateItem.index === root.currentIndex
|
|
||||||
? Qt.rgba(palette.highlightedText.r,
|
|
||||||
palette.highlightedText.g,
|
|
||||||
palette.highlightedText.b, 0.7)
|
|
||||||
: palette.mid
|
|
||||||
font.pixelSize: 11
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: hoverArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: {
|
|
||||||
root.currentIndex = delegateItem.index
|
|
||||||
root.selectionRequested()
|
|
||||||
}
|
|
||||||
onEntered: root.currentIndex = delegateItem.index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
@@ -11,24 +10,19 @@ import UIControls
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool isInEditor: false
|
|
||||||
|
|
||||||
property alias saveButton: saveButtonId
|
property alias saveButton: saveButtonId
|
||||||
property alias loadButton: loadButtonId
|
property alias loadButton: loadButtonId
|
||||||
property alias clearButton: clearButtonId
|
property alias clearButton: clearButtonId
|
||||||
property alias newChatButton: newChatButtonId
|
|
||||||
property alias tokensBadge: tokensBadgeId
|
property alias tokensBadge: tokensBadgeId
|
||||||
property alias recentPath: recentPathId
|
property alias recentPath: recentPathId
|
||||||
property alias openChatHistory: openChatHistoryId
|
property alias openChatHistory: openChatHistoryId
|
||||||
property alias pinButton: pinButtonId
|
property alias pinButton: pinButtonId
|
||||||
property alias relocateButton: relocateButtonId
|
|
||||||
property alias contextButton: contextButtonId
|
property alias contextButton: contextButtonId
|
||||||
property alias toolsButton: toolsButtonId
|
property alias toolsButton: toolsButtonId
|
||||||
property alias thinkingMode: thinkingModeId
|
property alias thinkingMode: thinkingModeId
|
||||||
property alias settingsButton: settingsButtonId
|
property alias settingsButton: settingsButtonId
|
||||||
property alias configSelector: configSelectorId
|
property alias configSelector: configSelectorId
|
||||||
property alias roleSelector: roleSelector
|
property alias roleSelector: roleSelector
|
||||||
property alias relocateTooltip: relocateTooltipId
|
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
Qt.darker(palette.window, 1.1) :
|
Qt.darker(palette.window, 1.1) :
|
||||||
@@ -61,77 +55,11 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: pinButtonId.hovered
|
ToolTip.text: checked ? qsTr("Unpin chat window")
|
||||||
delay: 250
|
|
||||||
text: pinButtonId.checked ? qsTr("Unpin chat window")
|
|
||||||
: qsTr("Pin chat window to the top")
|
: qsTr("Pin chat window to the top")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: relocateButtonId
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
id: relocateTooltipId
|
|
||||||
|
|
||||||
visible: relocateButtonId.hovered
|
|
||||||
delay: 250
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoASeparator {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: clearButtonId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
|
||||||
height: 15
|
|
||||||
width: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: clearButtonId.hovered
|
|
||||||
delay: 250
|
|
||||||
text: qsTr("Clean chat")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoASeparator {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: newChatButtonId
|
|
||||||
|
|
||||||
visible: root.isInEditor
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/new-chat-icon.svg"
|
|
||||||
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
|
||||||
height: 15
|
|
||||||
width: 15
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAToolTip {
|
|
||||||
visible: newChatButtonId.hovered
|
|
||||||
delay: 250
|
|
||||||
text: qsTr("Open new chat in a new tab")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAComboBox {
|
QoAComboBox {
|
||||||
id: configSelectorId
|
id: configSelectorId
|
||||||
@@ -141,11 +69,9 @@ Rectangle {
|
|||||||
model: []
|
model: []
|
||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: configSelectorId.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: qsTr("Switch saved AI configuration")
|
||||||
text: qsTr("Switch saved AI configuration")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAComboBox {
|
QoAComboBox {
|
||||||
@@ -156,11 +82,9 @@ Rectangle {
|
|||||||
model: []
|
model: []
|
||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: roleSelector.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: qsTr("Switch agent role (different system prompts)")
|
||||||
text: qsTr("Switch agent role (different system prompts)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,19 +107,17 @@ Rectangle {
|
|||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: toolsButtonId.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: {
|
||||||
text: {
|
|
||||||
if (!toolsButtonId.enabled) {
|
if (!toolsButtonId.enabled) {
|
||||||
return qsTr("Tools are disabled in General Settings")
|
return qsTr("Tools are disabled in General Settings")
|
||||||
}
|
}
|
||||||
return toolsButtonId.checked
|
return checked
|
||||||
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
||||||
: qsTr("Tools disabled: Simple conversation without tool access")
|
: qsTr("Tools disabled: Simple conversation without tool access")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: thinkingModeId
|
id: thinkingModeId
|
||||||
@@ -213,15 +135,12 @@ Rectangle {
|
|||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: thinkingModeId.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
|
||||||
text: thinkingModeId.enabled
|
|
||||||
? (thinkingModeId.checked ? qsTr("Thinking Mode enabled (Check model list support it)")
|
|
||||||
: qsTr("Thinking Mode disabled"))
|
: qsTr("Thinking Mode disabled"))
|
||||||
: qsTr("Thinking Mode is not available for this provider")
|
: qsTr("Thinking Mode is not available for this provider")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
id: settingsButtonId
|
id: settingsButtonId
|
||||||
@@ -235,11 +154,9 @@ Rectangle {
|
|||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: settingsButtonId.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
||||||
text: qsTr("Open Chat Assistant Settings")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoASeparator {
|
QoASeparator {
|
||||||
@@ -265,11 +182,9 @@ Rectangle {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: containsMouse
|
||||||
visible: parent.containsMouse && recentPathId.text.length > 0
|
ToolTip.delay: 500
|
||||||
text: recentPathId.text
|
ToolTip.text: recentPathId.text
|
||||||
delay: 500
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,12 +205,9 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: saveButtonId.hovered
|
ToolTip.text: qsTr("Save chat to *.json file")
|
||||||
delay: 250
|
|
||||||
text: qsTr("Save chat to *.json file")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -306,12 +218,9 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 8
|
width: 8
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: loadButtonId.hovered
|
ToolTip.text: qsTr("Load chat from *.json file")
|
||||||
delay: 250
|
|
||||||
text: qsTr("Load chat from *.json file")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAButton {
|
QoAButton {
|
||||||
@@ -322,12 +231,9 @@ Rectangle {
|
|||||||
height: 15
|
height: 15
|
||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
QoAToolTip {
|
ToolTip.delay: 250
|
||||||
visible: openChatHistoryId.hovered
|
ToolTip.text: qsTr("Show in system")
|
||||||
delay: 250
|
|
||||||
text: qsTr("Show in system")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QoASeparator {}
|
QoASeparator {}
|
||||||
@@ -342,11 +248,9 @@ Rectangle {
|
|||||||
width: 15
|
width: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
QoAToolTip {
|
ToolTip.visible: hovered
|
||||||
visible: contextButtonId.hovered
|
ToolTip.delay: 250
|
||||||
delay: 250
|
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
|
||||||
text: qsTr("View chat context (system prompt, role, rules)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Badge {
|
Badge {
|
||||||
@@ -356,6 +260,21 @@ Rectangle {
|
|||||||
ToolTip.delay: 250
|
ToolTip.delay: 250
|
||||||
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QoASeparator {}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: clearButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Clean chat")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "ConfigurationManager.hpp"
|
#include "ConfigurationManager.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
21
LICENSE
21
LICENSE
@@ -1,24 +1,3 @@
|
|||||||
===============================================================
|
|
||||||
ADDITIONAL TERMS UNDER GPLv3 SECTION 7(b)
|
|
||||||
===============================================================
|
|
||||||
|
|
||||||
In accordance with Section 7(b) of the GNU General Public License v3.0,
|
|
||||||
the following additional attribution term applies to QodeAssist:
|
|
||||||
|
|
||||||
You must preserve all author attributions, copyright notices, and the
|
|
||||||
project name "QodeAssist" in all copies and modified versions,
|
|
||||||
including in source file headers, the plugin metadata
|
|
||||||
(QodeAssist.json.in), and the About dialog or equivalent user-facing
|
|
||||||
identification. Modified versions must be clearly marked as different
|
|
||||||
from the original.
|
|
||||||
|
|
||||||
This is a reasonable attribution requirement permitted under GPLv3
|
|
||||||
§7(b) and §7(c). It supplements the notice-preservation obligations of
|
|
||||||
§4 and §5.
|
|
||||||
|
|
||||||
Copyright (C) 2024-2026 Petr Mironychev
|
|
||||||
===============================================================
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
|
|
||||||
@@ -64,21 +63,6 @@ void LLMClientInterface::handleFullResponse(const QString &requestId, const QStr
|
|||||||
m_performanceLogger.endTimeMeasurement(requestId);
|
m_performanceLogger.endTimeMeasurement(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (!m_activeRequests.contains(requestId) || !info.usage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
|
||||||
LOG_MESSAGE(QString("Completion usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||||
{
|
{
|
||||||
auto it = m_activeRequests.find(requestId);
|
auto it = m_activeRequests.find(requestId);
|
||||||
@@ -341,12 +325,6 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
this,
|
this,
|
||||||
&LLMClientInterface::handleFullResponse,
|
&LLMClientInterface::handleFullResponse,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
|
||||||
this,
|
|
||||||
&LLMClientInterface::handleRequestFinalized,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
provider->client(),
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
@@ -354,9 +332,6 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
&LLMClientInterface::handleRequestFailed,
|
&LLMClientInterface::handleRequestFailed,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
|
||||||
static_cast<int>(m_generalSettings.requestTimeout() * 1000));
|
|
||||||
|
|
||||||
auto requestId
|
auto requestId
|
||||||
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
||||||
m_activeRequests[requestId] = {request, provider};
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
|
||||||
#include <languageclient/languageclientinterface.h>
|
#include <languageclient/languageclientinterface.h>
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
@@ -54,8 +52,6 @@ protected:
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||||
void handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*
|
|
||||||
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*
|
|
||||||
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.9.20",
|
"Version" : "0.9.12",
|
||||||
"CompatVersion" : "${IDE_VERSION}",
|
"CompatVersion" : "${IDE_VERSION}",
|
||||||
"Vendor" : "Petr Mironychev",
|
"Vendor" : "Petr Mironychev",
|
||||||
"VendorId" : "petrmironychev",
|
"VendorId" : "petrmironychev",
|
||||||
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
|
||||||
"License" : "GPLv3 with additional attribution terms (§7b) — see LICENSE",
|
"License" : "GPLv3",
|
||||||
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
|
||||||
"Url" : "https://github.com/Palm1r/QodeAssist",
|
"Url" : "https://github.com/Palm1r/QodeAssist",
|
||||||
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*
|
|
||||||
* Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "QodeAssistClient.hpp"
|
#include "QodeAssistClient.hpp"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -11,16 +10,4 @@ const char MENU_ID[] = "QodeAssist.Menu";
|
|||||||
|
|
||||||
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
|
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
|
||||||
|
|
||||||
const char QODE_ASSIST_CHAT_CONTEXT[] = "QodeAssist.ChatContext";
|
|
||||||
const char QODE_ASSIST_CHAT_NAV_ID[] = "QodeAssistChat";
|
|
||||||
const char QODE_ASSIST_CHAT_EDITOR_ID[] = "QodeAssist.ChatEditor";
|
|
||||||
|
|
||||||
const char QODE_ASSIST_SHOW_CHAT_ACTION[] = "QodeAssist.ShowChatView";
|
|
||||||
const char QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION[] = "QodeAssist.OpenChatWindow";
|
|
||||||
const char QODE_ASSIST_NEW_CHAT_ACTION[] = "QodeAssist.NewChat";
|
|
||||||
|
|
||||||
const char QODE_ASSIST_CHAT_SEND_MESSAGE[] = "QodeAssist.Chat.SendMessage";
|
|
||||||
const char QODE_ASSIST_CHAT_CLEAR_SESSION[] = "QodeAssist.Chat.ClearSession";
|
|
||||||
const char QODE_ASSIST_CHAT_SHOW_IN_RIGHT[] = "QodeAssist.Chat.ShowInRightSidebar";
|
|
||||||
|
|
||||||
} // namespace QodeAssist::Constants
|
} // namespace QodeAssist::Constants
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "QuickRefactorHandler.hpp"
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
|
||||||
@@ -144,9 +143,6 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
|||||||
provider->client()->setMaxToolContinuations(
|
provider->client()->setMaxToolContinuations(
|
||||||
Settings::toolsSettings().maxToolContinuations());
|
Settings::toolsSettings().maxToolContinuations());
|
||||||
|
|
||||||
provider->client()->setTransferTimeout(
|
|
||||||
static_cast<int>(Settings::generalSettings().requestTimeout() * 1000));
|
|
||||||
|
|
||||||
m_isRefactoringInProgress = true;
|
m_isRefactoringInProgress = true;
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
@@ -156,13 +152,6 @@ void QuickRefactorHandler::prepareAndSendRequest(
|
|||||||
&QuickRefactorHandler::handleFullResponse,
|
&QuickRefactorHandler::handleFullResponse,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
connect(
|
|
||||||
provider->client(),
|
|
||||||
&::LLMQore::BaseClient::requestFinalized,
|
|
||||||
this,
|
|
||||||
&QuickRefactorHandler::handleRequestFinalized,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider->client(),
|
provider->client(),
|
||||||
&::LLMQore::BaseClient::requestFailed,
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
@@ -389,26 +378,26 @@ void QuickRefactorHandler::handleLLMResponse(
|
|||||||
|
|
||||||
void QuickRefactorHandler::cancelRequest()
|
void QuickRefactorHandler::cancelRequest()
|
||||||
{
|
{
|
||||||
if (!m_isRefactoringInProgress)
|
if (m_isRefactoringInProgress) {
|
||||||
return;
|
auto id = m_lastRequestId;
|
||||||
|
|
||||||
const auto id = m_lastRequestId;
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
m_isRefactoringInProgress = false;
|
if (it.key() == id) {
|
||||||
m_lastRequestId.clear();
|
const RequestContext &ctx = it.value();
|
||||||
|
ctx.provider->cancelRequest(id);
|
||||||
auto it = m_activeRequests.find(id);
|
|
||||||
if (it != m_activeRequests.end()) {
|
|
||||||
auto provider = it.value().provider;
|
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
if (provider)
|
break;
|
||||||
provider->cancelRequest(id);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_isRefactoringInProgress = false;
|
||||||
|
|
||||||
RefactorResult result;
|
RefactorResult result;
|
||||||
result.success = false;
|
result.success = false;
|
||||||
result.errorMessage = "Refactoring request was cancelled";
|
result.errorMessage = "Refactoring request was cancelled";
|
||||||
emit refactoringCompleted(result);
|
emit refactoringCompleted(result);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
|
||||||
{
|
{
|
||||||
@@ -419,22 +408,6 @@ void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info)
|
|
||||||
{
|
|
||||||
if (requestId != m_lastRequestId || !info.usage)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const auto &u = *info.usage;
|
|
||||||
LOG_MESSAGE(
|
|
||||||
QString("Quick refactor usage [%1]: prompt=%2 completion=%3 cached=%4 reasoning=%5")
|
|
||||||
.arg(requestId)
|
|
||||||
.arg(u.promptTokens)
|
|
||||||
.arg(u.completionTokens)
|
|
||||||
.arg(u.cachedPromptTokens)
|
|
||||||
.arg(u.reasoningTokens));
|
|
||||||
}
|
|
||||||
|
|
||||||
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
|
||||||
{
|
{
|
||||||
if (requestId == m_lastRequestId) {
|
if (requestId == m_lastRequestId) {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
#include <LLMQore/BaseClient.hpp>
|
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
#include <utils/textutils.h>
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
@@ -45,8 +43,6 @@ signals:
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleFullResponse(const QString &requestId, const QString &fullText);
|
void handleFullResponse(const QString &requestId, const QString &fullText);
|
||||||
void handleRequestFinalized(
|
|
||||||
const ::LLMQore::RequestID &requestId, const ::LLMQore::CompletionInfo &info);
|
|
||||||
void handleRequestFailed(const QString &requestId, const QString &error);
|
void handleRequestFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
117
README.md
117
README.md
@@ -6,7 +6,7 @@
|
|||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://discord.gg/BGMkUsXUgf)
|
[](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, Qwen, DeepSeek), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
|
 **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
|
||||||
|
|
||||||
⚠️ **Important Notice About Paid Providers**
|
⚠️ **Important Notice About Paid Providers**
|
||||||
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
|
||||||
@@ -35,12 +35,10 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
|
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
|
||||||
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
|
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
|
||||||
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
|
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
|
||||||
- **Agent Skills** — reusable folders of specialized instructions loaded on demand; discovered from `.qodeassist/skills/` and `.claude/skills/`, invoked automatically, with `/skill`, or always-on
|
|
||||||
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
|
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
|
||||||
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
|
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
|
||||||
- **File Context** — attach, link, or auto-sync open editor files for richer prompts
|
- **File Context** — attach, link, or auto-sync open editor files for richer prompts
|
||||||
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, Qwen (OpenAI + Responses), DeepSeek, any OpenAI-compatible endpoint
|
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, any OpenAI-compatible endpoint
|
||||||
- **Reasoning / Thinking** — streamed chain-of-thought is shown for reasoning models across Claude, Google, OpenAI Responses, and any OpenAI-compatible endpoint that returns `reasoning_content` (DeepSeek, Qwen QwQ/Qwen3-Thinking, LM Studio, OpenRouter, …)
|
|
||||||
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
|
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
|
||||||
|
|
||||||
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
||||||
@@ -55,11 +53,6 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
|
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Chat View Mode: (click to expand)</summary>
|
|
||||||
<img src="https://github.com/user-attachments/assets/5914dd78-c8a4-4d35-889a-10ec493d4c4b" width="600" alt="QodeAssistChat2">
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Multiline Code completion: (click to expand)</summary>
|
<summary>Multiline Code completion: (click to expand)</summary>
|
||||||
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
|
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
|
||||||
@@ -92,27 +85,7 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
|||||||
|
|
||||||
## Install plugin to QtCreator
|
## Install plugin to QtCreator
|
||||||
|
|
||||||
### Method 1: Using the Extension Registry (Recommended)
|
### Method 1: Using QodeAssistUpdater (Beta)
|
||||||
|
|
||||||
You can install and update QodeAssist directly from within Qt Creator by adding the QodeAssist registry as an external extension repository.
|
|
||||||
|
|
||||||
1. Open the Extensions page (`Qt Creator → Extensions`) and switch to the **Browser** tab
|
|
||||||
2. Enable **Use External Repository**
|
|
||||||
3. Next to **Repository URLs**, click **Add** and paste the registry archive URL matching your Qt Creator version:
|
|
||||||
- **Latest (QtC 19)**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist.tar.gz`
|
|
||||||
- **QtC 19**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc19.tar.gz`
|
|
||||||
- **QtC 18**: `https://github.com/Palm1r/extension-registry/archive/refs/heads/qodeassist-qtc18.tar.gz`
|
|
||||||
<details>
|
|
||||||
<summary>Example of extension registry: (click to expand)</summary>
|
|
||||||
<img width="600" alt="RegistryExample" src="https://github.com/user-attachments/assets/8ab8cf10-72e7-4961-8c5a-21d530378a05">
|
|
||||||
</details>
|
|
||||||
|
|
||||||
4. Click **Apply** — QodeAssist will appear in the extensions list, where you can **Install** it
|
|
||||||
5. Updates can be installed from the same screen when a new version is published
|
|
||||||
|
|
||||||
> **Note:** This is an external repository not maintained by The Qt Company. By adding it you accept responsibility for managing the associated risks, as stated in the Extensions page.
|
|
||||||
|
|
||||||
### Method 2: Using QodeAssistUpdater (Beta)
|
|
||||||
|
|
||||||
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
|
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
|
||||||
|
|
||||||
@@ -140,7 +113,7 @@ Download pre-built binary from [QodeAssistUpdater releases](https://github.com/P
|
|||||||
|
|
||||||
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
|
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
|
||||||
|
|
||||||
### Method 3: Manual Installation
|
### Method 2: Manual Installation
|
||||||
|
|
||||||
1. Install Latest Qt Creator
|
1. Install Latest Qt Creator
|
||||||
2. Download the QodeAssist plugin for your Qt Creator
|
2. Download the QodeAssist plugin for your Qt Creator
|
||||||
@@ -170,8 +143,6 @@ The Quick Setup feature provides one-click configuration for popular cloud AI mo
|
|||||||
- **OpenAI** (gpt-5.2-codex)
|
- **OpenAI** (gpt-5.2-codex)
|
||||||
- **Mistral AI** (Codestral 2501)
|
- **Mistral AI** (Codestral 2501)
|
||||||
- **Google AI** (Gemini 2.5 Flash)
|
- **Google AI** (Gemini 2.5 Flash)
|
||||||
- **Qwen** (Qwen3.6 Plus, Qwen3.7 Max)
|
|
||||||
- **DeepSeek** (DeepSeek V4 Flash, DeepSeek V4 Pro)
|
|
||||||
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
||||||
|
|
||||||
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
||||||
@@ -192,8 +163,6 @@ For advanced users or local models, choose your preferred provider and follow th
|
|||||||
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
|
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
|
||||||
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
|
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
|
||||||
- **[Google AI](docs/google-ai-configuration.md)** — Gemini
|
- **[Google AI](docs/google-ai-configuration.md)** — Gemini
|
||||||
- **Qwen (Alibaba)** — DashScope OpenAI-compatible Chat and Responses endpoints
|
|
||||||
- **DeepSeek** — `deepseek-chat` and `deepseek-reasoner` (reasoning shown as thinking)
|
|
||||||
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
||||||
|
|
||||||
### Recommended Models for Best Experience
|
### Recommended Models for Best Experience
|
||||||
@@ -258,7 +227,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
|
|||||||
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
||||||
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
- **[File Context](docs/file-context.md)** - Attach or link files for better context
|
||||||
- Automatic syncing with open editor files (optional)
|
- Automatic syncing with open editor files (optional)
|
||||||
- Extended thinking / reasoning mode - shows streamed chain-of-thought for reasoning models (Claude, Google, OpenAI Responses, and OpenAI-compatible endpoints returning `reasoning_content` such as DeepSeek, Qwen, LM Studio, OpenRouter)
|
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
|
||||||
|
|
||||||
### Quick Refactoring
|
### Quick Refactoring
|
||||||
- Inline code refactoring directly in the editor with AI assistance
|
- Inline code refactoring directly in the editor with AI assistance
|
||||||
@@ -284,41 +253,6 @@ Chat and Quick Refactor can call tools to inspect and modify your project. Each
|
|||||||
| `execute_terminal_command` | Run a shell command (with confirmation) |
|
| `execute_terminal_command` | Run a shell command (with confirmation) |
|
||||||
| `todo_tool` | Track multi-step task progress during a conversation |
|
| `todo_tool` | Track multi-step task progress during a conversation |
|
||||||
|
|
||||||
### Skills
|
|
||||||
|
|
||||||
**Agent Skills** package specialized instructions and workflows into reusable folders the AI loads on demand. QodeAssist implements the open [Agent Skills](https://agentskills.io) format, so skills authored for Claude Code, Cursor, or other agents work as-is.
|
|
||||||
|
|
||||||
A skill is a folder containing a `SKILL.md` file — YAML frontmatter (`name`, `description`) plus Markdown instructions:
|
|
||||||
|
|
||||||
```
|
|
||||||
my-skill/
|
|
||||||
└── SKILL.md
|
|
||||||
```
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
name: my-skill
|
|
||||||
description: What the skill does and when to use it.
|
|
||||||
---
|
|
||||||
|
|
||||||
# My Skill
|
|
||||||
|
|
||||||
Step-by-step instructions for the task...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Where skills are discovered:**
|
|
||||||
- **Project skills** — project-relative subdirectories (default `.qodeassist/skills/` and `.claude/skills/`), configured in `Projects → QodeAssist → Skills`. Project skills win over global ones on a name collision.
|
|
||||||
- **Global skills** — absolute directories shared across all projects (default includes `~/.claude/skills/`), configured in `Tools → Options → QodeAssist → Skills`.
|
|
||||||
|
|
||||||
Both settings pages show the list of currently discovered skills.
|
|
||||||
|
|
||||||
**How skills are used in Chat:**
|
|
||||||
- **Automatically** — each skill's name and description is added to the system prompt; when a request matches, the model loads the full instructions via the `load_skill` tool (requires a tool-calling model).
|
|
||||||
- **Explicitly** — type `/` in the chat input and pick a skill from the popup; its instructions are injected into that one message. Works with any model.
|
|
||||||
- **Always-on** — a skill whose frontmatter has `metadata: always-on: "true"` is injected into every chat request automatically.
|
|
||||||
|
|
||||||
Enable or disable the whole feature in `Tools → Options → QodeAssist → Skills`.
|
|
||||||
|
|
||||||
### MCP Server
|
### MCP Server
|
||||||
|
|
||||||
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
|
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
|
||||||
@@ -520,7 +454,6 @@ For additional support, join our [Discord Community](https://discord.gg/BGMkUsXU
|
|||||||
- [x] Quick refactoring with custom-instructions library
|
- [x] Quick refactoring with custom-instructions library
|
||||||
- [x] Diff sharing with models
|
- [x] Diff sharing with models
|
||||||
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
||||||
- [x] Agent Skills (project + global directories, `/skill` commands, always-on, `load_skill` tool)
|
|
||||||
- [x] Project-specific rules (`.qodeassist/rules/`)
|
- [x] Project-specific rules (`.qodeassist/rules/`)
|
||||||
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
||||||
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
||||||
@@ -537,7 +470,6 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
|||||||
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
3. **Spread the Word**: Star our GitHub repository and share QodeAssist with your fellow developers.
|
||||||
|
|
||||||
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
4. **Financial Support**: If you'd like to support the development financially, you can make a donation using one of the following:
|
||||||
- Paypal: [my paypalme page](https://www.paypal.com/paypalme/palm1r)
|
|
||||||
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
- Bitcoin (BTC): `bc1qndq7f0mpnlya48vk7kugvyqj5w89xrg4wzg68t`
|
||||||
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
- Ethereum (ETH): `0xA5e8c37c94b24e25F9f1f292a01AF55F03099D8D`
|
||||||
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
- Litecoin (LTC): `ltc1qlrxnk30s2pcjchzx4qrxvdjt5gzuervy5mv0vy`
|
||||||
@@ -590,45 +522,6 @@ cmake --build .
|
|||||||
|
|
||||||
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
|
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
QodeAssist is licensed under the **GNU General Public License v3.0**
|
|
||||||
(see [`LICENSE`](LICENSE)), with **additional attribution terms under
|
|
||||||
GPLv3 Section 7(b)**.
|
|
||||||
|
|
||||||
You are free to use, modify, and redistribute QodeAssist under GPL-3.0,
|
|
||||||
but you **must preserve** the original author attribution, copyright
|
|
||||||
notices, and project identification — including in source file headers,
|
|
||||||
the plugin metadata (`QodeAssist.json.in`), and the About dialog or
|
|
||||||
equivalent user-facing identification. Modified versions must be clearly
|
|
||||||
marked as different from the original.
|
|
||||||
|
|
||||||
### Commercial licensing
|
|
||||||
|
|
||||||
QodeAssist is also available under a separate commercial license for use
|
|
||||||
in proprietary or closed-source products without GPL-3.0 obligations.
|
|
||||||
For commercial licensing inquiries, contact **palm1r-github-dev@pm.me**.
|
|
||||||
|
|
||||||
### Qt Creator components and attributions
|
|
||||||
|
|
||||||
QodeAssist is a plugin for Qt Creator and incorporates certain components
|
|
||||||
(plugin templates, API headers, and related boilerplate) originating from
|
|
||||||
Qt Creator, which are copyright (C) The Qt Company Ltd.
|
|
||||||
|
|
||||||
These components are provided by The Qt Company under the GNU General
|
|
||||||
Public License version 3, annotated with **The Qt Company GPL Exception
|
|
||||||
1.0**. This exception permits the development and distribution of Qt
|
|
||||||
Creator plugins under licenses of the plugin author's own choosing,
|
|
||||||
notwithstanding the GPL's general linking requirements. It is this
|
|
||||||
exception that allows QodeAssist to be offered under both GPL-3.0 and a
|
|
||||||
separate commercial license.
|
|
||||||
|
|
||||||
The original copyright and license notices of The Qt Company are
|
|
||||||
preserved in the relevant source files and must not be removed.
|
|
||||||
|
|
||||||
For Qt Creator's licensing terms, see
|
|
||||||
[LICENSE.GPL3-EXCEPT](https://github.com/qt-creator/qt-creator/blob/master/LICENSES/LICENSE.GPL3-EXCEPT).
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "RefactorSuggestion.hpp"
|
#include "RefactorSuggestion.hpp"
|
||||||
#include "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2024-2026 Petr Mironychev
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "RefactorSuggestionHoverHandler.hpp"
|
#include "RefactorSuggestionHoverHandler.hpp"
|
||||||
#include "RefactorSuggestion.hpp"
|
#include "RefactorSuggestion.hpp"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowEditor.hpp"
|
#include "FlowEditor.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowItem.hpp"
|
#include "FlowItem.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "FlowsModel.hpp"
|
#include "FlowsModel.hpp"
|
||||||
|
|
||||||
#include "FlowManager.hpp"
|
#include "FlowManager.hpp"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "GridBackground.hpp"
|
#include "GridBackground.hpp"
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskConnectionItem.hpp"
|
#include "TaskConnectionItem.hpp"
|
||||||
#include "TaskItem.hpp"
|
#include "TaskItem.hpp"
|
||||||
#include "TaskPortItem.hpp"
|
#include "TaskPortItem.hpp"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "TaskConnection.hpp"
|
#include "TaskConnection.hpp"
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskConnectionsModel.hpp"
|
#include "TaskConnectionsModel.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskItem.hpp"
|
#include "TaskItem.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#include "TaskModel.hpp"
|
#include "TaskModel.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::TaskFlow {
|
namespace QodeAssist::TaskFlow {
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// Copyright (C) 2025-2026 Petr Mironychev
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user