mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 10:59:30 -04:00
refactor: Move out InputTokenCounter, FileEditController, ChatHistoryStore, ChatConfigurationController
This commit is contained in:
@@ -70,6 +70,10 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
ChatFileManager.hpp ChatFileManager.cpp
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
ChatCompressor.hpp ChatCompressor.cpp
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
AgentRoleController.hpp AgentRoleController.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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
99
ChatView/ChatConfigurationController.cpp
Normal file
99
ChatView/ChatConfigurationController.cpp
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "ChatConfigurationController.hpp"
|
||||||
|
|
||||||
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
|
#include "ConfigurationManager.hpp"
|
||||||
|
#include "GeneralSettings.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatConfigurationController::ChatConfigurationController(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
auto &settings = Settings::generalSettings();
|
||||||
|
connect(
|
||||||
|
&settings.caProvider,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatConfigurationController::updateCurrentConfiguration);
|
||||||
|
connect(
|
||||||
|
&settings.caModel,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&ChatConfigurationController::updateCurrentConfiguration);
|
||||||
|
|
||||||
|
loadAvailableConfigurations();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList ChatConfigurationController::availableConfigurations() const
|
||||||
|
{
|
||||||
|
return m_availableConfigurations;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatConfigurationController::currentConfiguration() const
|
||||||
|
{
|
||||||
|
return m_currentConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatConfigurationController::updateCurrentConfiguration()
|
||||||
|
{
|
||||||
|
auto &settings = Settings::generalSettings();
|
||||||
|
m_currentConfiguration
|
||||||
|
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
||||||
|
emit currentConfigurationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatConfigurationController::loadAvailableConfigurations()
|
||||||
|
{
|
||||||
|
auto &manager = Settings::ConfigurationManager::instance();
|
||||||
|
manager.loadConfigurations(Settings::ConfigurationType::Chat);
|
||||||
|
|
||||||
|
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||||
|
Settings::ConfigurationType::Chat);
|
||||||
|
|
||||||
|
m_availableConfigurations.clear();
|
||||||
|
m_availableConfigurations.append(QObject::tr("Current Settings"));
|
||||||
|
|
||||||
|
for (const Settings::AIConfiguration &config : configs) {
|
||||||
|
m_availableConfigurations.append(config.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentConfiguration();
|
||||||
|
|
||||||
|
emit availableConfigurationsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatConfigurationController::applyConfiguration(const QString &configName)
|
||||||
|
{
|
||||||
|
if (configName == QObject::tr("Current Settings")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &manager = Settings::ConfigurationManager::instance();
|
||||||
|
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||||
|
Settings::ConfigurationType::Chat);
|
||||||
|
|
||||||
|
for (const Settings::AIConfiguration &config : configs) {
|
||||||
|
if (config.name == configName) {
|
||||||
|
auto &settings = Settings::generalSettings();
|
||||||
|
|
||||||
|
settings.caProvider.setValue(config.provider);
|
||||||
|
settings.caModel.setValue(config.model);
|
||||||
|
settings.caTemplate.setValue(config.templateName);
|
||||||
|
settings.caUrl.setValue(config.url);
|
||||||
|
settings.caCustomEndpoint.setValue(config.customEndpoint);
|
||||||
|
|
||||||
|
settings.writeSettings();
|
||||||
|
|
||||||
|
m_currentConfiguration = QString("%1 - %2").arg(config.provider, config.model);
|
||||||
|
emit currentConfigurationChanged();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
35
ChatView/ChatConfigurationController.hpp
Normal file
35
ChatView/ChatConfigurationController.hpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatConfigurationController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatConfigurationController(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QStringList availableConfigurations() const;
|
||||||
|
QString currentConfiguration() const;
|
||||||
|
|
||||||
|
void loadAvailableConfigurations();
|
||||||
|
void applyConfiguration(const QString &configName);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void availableConfigurationsChanged();
|
||||||
|
void currentConfigurationChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateCurrentConfiguration();
|
||||||
|
|
||||||
|
QStringList m_availableConfigurations;
|
||||||
|
QString m_currentConfiguration;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
228
ChatView/ChatHistoryStore.cpp
Normal file
228
ChatView/ChatHistoryStore.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "ChatHistoryStore.hpp"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
#include "ChatModel.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "ProjectSettings.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatHistoryStore::ChatHistoryStore(ChatModel *chatModel, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_chatModel(chatModel)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::historyDir() const
|
||||||
|
{
|
||||||
|
QString path;
|
||||||
|
|
||||||
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
|
} else {
|
||||||
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists() && !dir.mkpath(".")) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create directory: %1").arg(path));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::suggestedFileName() const
|
||||||
|
{
|
||||||
|
QString shortMessage;
|
||||||
|
|
||||||
|
if (m_chatModel->rowCount() > 0) {
|
||||||
|
QString firstMessage
|
||||||
|
= m_chatModel->data(m_chatModel->index(0), ChatModel::Content).toString();
|
||||||
|
shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||||
|
|
||||||
|
if (shortMessage.isEmpty()) {
|
||||||
|
QVariantList images
|
||||||
|
= m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList();
|
||||||
|
if (!images.isEmpty()) {
|
||||||
|
shortMessage = "image_chat";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateChatFileName(shortMessage, historyDir());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::autosaveFilePath(const QString &recentFilePath) const
|
||||||
|
{
|
||||||
|
if (!recentFilePath.isEmpty()) {
|
||||||
|
return recentFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString dir = historyDir();
|
||||||
|
if (dir.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return QDir(dir).filePath(suggestedFileName() + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::autosaveFilePath(
|
||||||
|
const QString &recentFilePath, const QString &firstMessage, bool hasImageAttachments) const
|
||||||
|
{
|
||||||
|
if (!recentFilePath.isEmpty()) {
|
||||||
|
return recentFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString dir = historyDir();
|
||||||
|
if (dir.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
||||||
|
|
||||||
|
if (shortMessage.isEmpty() && hasImageAttachments) {
|
||||||
|
shortMessage = "image_chat";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString fileName = generateChatFileName(shortMessage, dir);
|
||||||
|
return QDir(dir).filePath(fileName + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
SerializationResult ChatHistoryStore::save(const QString &filePath) const
|
||||||
|
{
|
||||||
|
return ChatSerializer::saveToFile(m_chatModel, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
SerializationResult ChatHistoryStore::load(const QString &filePath) const
|
||||||
|
{
|
||||||
|
return ChatSerializer::loadFromFile(m_chatModel, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatHistoryStore::showSaveDialog()
|
||||||
|
{
|
||||||
|
QString initialDir = historyDir();
|
||||||
|
|
||||||
|
QFileDialog *dialog = new QFileDialog(nullptr, tr("Save Chat History"));
|
||||||
|
dialog->setAcceptMode(QFileDialog::AcceptSave);
|
||||||
|
dialog->setFileMode(QFileDialog::AnyFile);
|
||||||
|
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||||
|
dialog->setDefaultSuffix("json");
|
||||||
|
if (!initialDir.isEmpty()) {
|
||||||
|
dialog->setDirectory(initialDir);
|
||||||
|
dialog->selectFile(suggestedFileName() + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||||
|
if (result == QFileDialog::Accepted) {
|
||||||
|
QStringList files = dialog->selectedFiles();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
emit saveRequested(files.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatHistoryStore::showLoadDialog()
|
||||||
|
{
|
||||||
|
QString initialDir = historyDir();
|
||||||
|
|
||||||
|
QFileDialog *dialog = new QFileDialog(nullptr, tr("Load Chat History"));
|
||||||
|
dialog->setAcceptMode(QFileDialog::AcceptOpen);
|
||||||
|
dialog->setFileMode(QFileDialog::ExistingFile);
|
||||||
|
dialog->setNameFilter(tr("JSON files (*.json)"));
|
||||||
|
if (!initialDir.isEmpty()) {
|
||||||
|
dialog->setDirectory(initialDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
||||||
|
if (result == QFileDialog::Accepted) {
|
||||||
|
QStringList files = dialog->selectedFiles();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
emit loadRequested(files.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->open();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatHistoryStore::openHistoryFolder() const
|
||||||
|
{
|
||||||
|
QString path;
|
||||||
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
||||||
|
Settings::ProjectSettings projectSettings(project);
|
||||||
|
path = projectSettings.chatHistoryPath().toFSPathString();
|
||||||
|
} else {
|
||||||
|
QDir baseDir(Core::ICore::userResourcePath().toFSPathString());
|
||||||
|
path = baseDir.filePath("qodeassist/chat_history");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(path);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.mkpath(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||||
|
QDesktopServices::openUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatHistoryStore::generateChatFileName(const QString &shortMessage, const QString &dir) const
|
||||||
|
{
|
||||||
|
static const QRegularExpression saitizeSymbols = QRegularExpression("[\\/:*?\"<>|\\s]");
|
||||||
|
static const QRegularExpression underSymbols = QRegularExpression("_+");
|
||||||
|
|
||||||
|
QStringList parts;
|
||||||
|
QString sanitizedMessage = shortMessage;
|
||||||
|
sanitizedMessage.replace(saitizeSymbols, "_");
|
||||||
|
sanitizedMessage.replace(underSymbols, "_");
|
||||||
|
sanitizedMessage = sanitizedMessage.trimmed();
|
||||||
|
|
||||||
|
if (!sanitizedMessage.isEmpty()) {
|
||||||
|
if (sanitizedMessage.startsWith('_')) {
|
||||||
|
sanitizedMessage.remove(0, 1);
|
||||||
|
}
|
||||||
|
if (sanitizedMessage.endsWith('_')) {
|
||||||
|
sanitizedMessage.chop(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString fullPath = QDir(dir).filePath(sanitizedMessage);
|
||||||
|
QFileInfo fileInfo(fullPath);
|
||||||
|
if (!fileInfo.exists() && QFileInfo(fileInfo.path()).isWritable()) {
|
||||||
|
parts << sanitizedMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts << QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm");
|
||||||
|
|
||||||
|
QString fileName = parts.join("_");
|
||||||
|
QString fullPath = QDir(dir).filePath(fileName);
|
||||||
|
QFileInfo finalCheck(fullPath);
|
||||||
|
|
||||||
|
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
|
||||||
|
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
47
ChatView/ChatHistoryStore.hpp
Normal file
47
ChatView/ChatHistoryStore.hpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "ChatSerializer.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatModel;
|
||||||
|
|
||||||
|
class ChatHistoryStore : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatHistoryStore(ChatModel *chatModel, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString historyDir() const;
|
||||||
|
QString suggestedFileName() const;
|
||||||
|
QString autosaveFilePath(const QString &recentFilePath) const;
|
||||||
|
QString autosaveFilePath(
|
||||||
|
const QString &recentFilePath,
|
||||||
|
const QString &firstMessage,
|
||||||
|
bool hasImageAttachments) const;
|
||||||
|
|
||||||
|
SerializationResult save(const QString &filePath) const;
|
||||||
|
SerializationResult load(const QString &filePath) const;
|
||||||
|
|
||||||
|
void showSaveDialog();
|
||||||
|
void showLoadDialog();
|
||||||
|
void openHistoryFolder() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void saveRequested(const QString &filePath);
|
||||||
|
void loadRequested(const QString &filePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||||
|
|
||||||
|
ChatModel *m_chatModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -3,12 +3,7 @@
|
|||||||
|
|
||||||
#include "ChatRootView.hpp"
|
#include "ChatRootView.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include <LLMQore/ToolsManager.hpp>
|
|
||||||
#include <QClipboard>
|
#include <QClipboard>
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
@@ -18,27 +13,24 @@
|
|||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
#include <coreplugin/icore.h>
|
|
||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectexplorer.h>
|
#include <projectexplorer/projectexplorer.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
#include <texteditor/texteditor.h>
|
|
||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
#include "AgentRoleController.hpp"
|
#include "AgentRoleController.hpp"
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "ChatConfigurationController.hpp"
|
||||||
#include "ChatCompressor.hpp"
|
#include "ChatCompressor.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatHistoryStore.hpp"
|
||||||
#include "ConfigurationManager.hpp"
|
#include "FileEditController.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "InputTokenCounter.hpp"
|
||||||
#include "SettingsConstants.hpp"
|
#include "SettingsConstants.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
#include "context/ChangesManager.h"
|
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
#include "context/TokenUtils.hpp"
|
|
||||||
#include "pluginllmcore/RulesLoader.hpp"
|
#include "pluginllmcore/RulesLoader.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@@ -52,6 +44,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
, m_isRequestInProgress(false)
|
, m_isRequestInProgress(false)
|
||||||
, m_chatCompressor(new ChatCompressor(this))
|
, m_chatCompressor(new ChatCompressor(this))
|
||||||
, m_agentRoleController(new AgentRoleController(this))
|
, m_agentRoleController(new AgentRoleController(this))
|
||||||
|
, m_configurationController(new ChatConfigurationController(this))
|
||||||
|
, m_fileEditController(new FileEditController(m_chatModel, this))
|
||||||
|
, m_tokenCounter(
|
||||||
|
new InputTokenCounter(m_chatModel, m_clientInterface->contextManager(), this))
|
||||||
|
, m_historyStore(new ChatHistoryStore(m_chatModel, this))
|
||||||
{
|
{
|
||||||
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
|
||||||
connect(
|
connect(
|
||||||
@@ -65,19 +62,16 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
connect(
|
connect(
|
||||||
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
||||||
|
|
||||||
connect(&settings.caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
connect(
|
||||||
auto &settings = Settings::generalSettings();
|
m_configurationController,
|
||||||
m_currentConfiguration
|
&ChatConfigurationController::availableConfigurationsChanged,
|
||||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
this,
|
||||||
emit currentConfigurationChanged();
|
&ChatRootView::availableConfigurationsChanged);
|
||||||
});
|
connect(
|
||||||
|
m_configurationController,
|
||||||
connect(&settings.caModel, &Utils::BaseAspect::changed, this, [this]() {
|
&ChatConfigurationController::currentConfigurationChanged,
|
||||||
auto &settings = Settings::generalSettings();
|
this,
|
||||||
m_currentConfiguration
|
&ChatRootView::currentConfigurationChanged);
|
||||||
= QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
@@ -97,37 +91,21 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
|
|
||||||
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
||||||
setRecentFilePath(QString{});
|
setRecentFilePath(QString{});
|
||||||
m_currentMessageRequestId.clear();
|
m_fileEditController->clearCurrentRequestId();
|
||||||
updateCurrentMessageEditsStats();
|
});
|
||||||
|
connect(this, &ChatRootView::attachmentFilesChanged, this, [this]() {
|
||||||
|
m_tokenCounter->setAttachments(m_attachmentFiles);
|
||||||
|
});
|
||||||
|
connect(this, &ChatRootView::linkedFilesChanged, this, [this]() {
|
||||||
|
m_tokenCounter->setLinkedFiles(m_linkedFiles);
|
||||||
});
|
});
|
||||||
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
|
|
||||||
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().useSystemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::updateInputTokensCount);
|
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().systemPrompt,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::updateInputTokensCount);
|
|
||||||
connect(this, &ChatRootView::useToolsChanged, this, &ChatRootView::updateInputTokensCount);
|
connect(this, &ChatRootView::useToolsChanged, this, &ChatRootView::updateInputTokensCount);
|
||||||
connect(
|
|
||||||
&Settings::chatAssistantSettings().enableChatTools,
|
|
||||||
&Utils::BaseAspect::changed,
|
|
||||||
this,
|
|
||||||
&ChatRootView::updateInputTokensCount);
|
|
||||||
|
|
||||||
rewireToolsChangedConnection();
|
|
||||||
connect(
|
connect(
|
||||||
&Settings::generalSettings().caProvider,
|
m_tokenCounter,
|
||||||
&Utils::BaseAspect::changed,
|
&InputTokenCounter::inputTokensChanged,
|
||||||
this,
|
this,
|
||||||
[this]() {
|
&ChatRootView::inputTokensCountChanged);
|
||||||
rewireToolsChangedConnection();
|
|
||||||
updateInputTokensCount();
|
|
||||||
});
|
|
||||||
connect(
|
connect(
|
||||||
m_agentRoleController,
|
m_agentRoleController,
|
||||||
&AgentRoleController::availableRolesChanged,
|
&AgentRoleController::availableRolesChanged,
|
||||||
@@ -191,72 +169,40 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
emit lastErrorMessageChanged();
|
emit lastErrorMessageChanged();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
|
connect(
|
||||||
if (!m_currentMessageRequestId.isEmpty()) {
|
m_clientInterface,
|
||||||
LOG_MESSAGE(
|
&ClientInterface::requestStarted,
|
||||||
QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId));
|
this,
|
||||||
}
|
[this](const QString &requestId) { m_fileEditController->setCurrentRequestId(requestId); });
|
||||||
|
|
||||||
m_currentMessageRequestId = requestId;
|
|
||||||
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
m_clientInterface,
|
m_clientInterface,
|
||||||
&ClientInterface::messageUsageReceived,
|
&ClientInterface::messageUsageReceived,
|
||||||
this,
|
this,
|
||||||
[this](int promptTokens, int /*completionTokens*/, int /*cached*/, int /*reasoning*/) {
|
[this](int promptTokens, int /*completionTokens*/, int /*cached*/, int /*reasoning*/) {
|
||||||
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
|
m_tokenCounter->recordServerUsage(promptTokens);
|
||||||
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));
|
|
||||||
|
|
||||||
updateInputTokensCount();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&Context::ChangesManager::instance(),
|
m_fileEditController,
|
||||||
&Context::ChangesManager::fileEditAdded,
|
&FileEditController::statsChanged,
|
||||||
this,
|
this,
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
&ChatRootView::currentMessageEditsStatsChanged);
|
||||||
|
connect(m_fileEditController, &FileEditController::infoMessage, this, [this](const QString &m) {
|
||||||
|
m_lastInfoMessage = m;
|
||||||
|
emit lastInfoMessageChanged();
|
||||||
|
});
|
||||||
|
connect(m_fileEditController, &FileEditController::errorOccurred, this, [this](const QString &e) {
|
||||||
|
m_lastErrorMessage = e;
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
});
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&Context::ChangesManager::instance(),
|
m_historyStore, &ChatHistoryStore::saveRequested, this, &ChatRootView::saveHistory);
|
||||||
&Context::ChangesManager::fileEditApplied,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&Context::ChangesManager::instance(),
|
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
||||||
&Context::ChangesManager::fileEditRejected,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditUndone,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
connect(
|
|
||||||
&Context::ChangesManager::instance(),
|
|
||||||
&Context::ChangesManager::fileEditArchived,
|
|
||||||
this,
|
|
||||||
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
|
||||||
|
|
||||||
updateInputTokensCount();
|
|
||||||
refreshRules();
|
refreshRules();
|
||||||
loadAvailableConfigurations();
|
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
ProjectExplorer::ProjectManager::instance(),
|
ProjectExplorer::ProjectManager::instance(),
|
||||||
@@ -362,7 +308,8 @@ bool ChatRootView::deferSendForAutoCompress(
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
const int threshold = settings.autoCompressThreshold();
|
const int threshold = settings.autoCompressThreshold();
|
||||||
if (m_inputTokensCount < threshold)
|
const int inputTokens = m_tokenCounter->inputTokens();
|
||||||
|
if (inputTokens < threshold)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (m_recentFilePath.isEmpty()) {
|
if (m_recentFilePath.isEmpty()) {
|
||||||
@@ -377,7 +324,7 @@ bool ChatRootView::deferSendForAutoCompress(
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Auto-compress preempt: estimated next=%1 ≥ threshold=%2; deferring send")
|
LOG_MESSAGE(QString("Auto-compress preempt: estimated next=%1 ≥ threshold=%2; deferring send")
|
||||||
.arg(m_inputTokensCount)
|
.arg(inputTokens)
|
||||||
.arg(threshold));
|
.arg(threshold));
|
||||||
|
|
||||||
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
|
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
|
||||||
@@ -400,9 +347,7 @@ void ChatRootView::dispatchSend(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_lastSentEstimate = m_calibrationFactor > 0.0
|
m_tokenCounter->recordSent();
|
||||||
? static_cast<int>(m_inputTokensCount / m_calibrationFactor)
|
|
||||||
: m_inputTokensCount;
|
|
||||||
|
|
||||||
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
||||||
|
|
||||||
@@ -449,27 +394,6 @@ void ChatRootView::clearMessages()
|
|||||||
clearLinkedFiles();
|
clearLinkedFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::getChatsHistoryDir() 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 ChatRootView::currentTemplate() const
|
QString ChatRootView::currentTemplate() const
|
||||||
{
|
{
|
||||||
auto &settings = Settings::generalSettings();
|
auto &settings = Settings::generalSettings();
|
||||||
@@ -478,7 +402,7 @@ QString ChatRootView::currentTemplate() const
|
|||||||
|
|
||||||
void ChatRootView::saveHistory(const QString &filePath)
|
void ChatRootView::saveHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
auto result = ChatSerializer::saveToFile(m_chatModel, filePath);
|
auto result = m_historyStore->save(filePath);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
|
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
|
||||||
} else {
|
} else {
|
||||||
@@ -488,7 +412,7 @@ void ChatRootView::saveHistory(const QString &filePath)
|
|||||||
|
|
||||||
void ChatRootView::loadHistory(const QString &filePath)
|
void ChatRootView::loadHistory(const QString &filePath)
|
||||||
{
|
{
|
||||||
auto result = ChatSerializer::loadFromFile(m_chatModel, filePath);
|
auto result = m_historyStore->load(filePath);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
||||||
} else {
|
} else {
|
||||||
@@ -502,82 +426,18 @@ void ChatRootView::loadHistory(const QString &filePath)
|
|||||||
emit attachmentFilesChanged();
|
emit attachmentFilesChanged();
|
||||||
emit linkedFilesChanged();
|
emit linkedFilesChanged();
|
||||||
|
|
||||||
m_currentMessageRequestId.clear();
|
m_fileEditController->clearCurrentRequestId();
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showSaveDialog()
|
void ChatRootView::showSaveDialog()
|
||||||
{
|
{
|
||||||
QString initialDir = getChatsHistoryDir();
|
m_historyStore->showSaveDialog();
|
||||||
|
|
||||||
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(getSuggestedFileName() + ".json");
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(dialog, &QFileDialog::finished, this, [this, dialog](int result) {
|
|
||||||
if (result == QFileDialog::Accepted) {
|
|
||||||
QStringList files = dialog->selectedFiles();
|
|
||||||
if (!files.isEmpty()) {
|
|
||||||
saveHistory(files.first());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialog->deleteLater();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog->open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showLoadDialog()
|
void ChatRootView::showLoadDialog()
|
||||||
{
|
{
|
||||||
QString initialDir = getChatsHistoryDir();
|
m_historyStore->showLoadDialog();
|
||||||
|
|
||||||
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()) {
|
|
||||||
loadHistory(files.first());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dialog->deleteLater();
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog->open();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString ChatRootView::getSuggestedFileName() 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, getChatsHistoryDir());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::autosave()
|
void ChatRootView::autosave()
|
||||||
@@ -588,45 +448,21 @@ void ChatRootView::autosave()
|
|||||||
|
|
||||||
QString filePath = getAutosaveFilePath();
|
QString filePath = getAutosaveFilePath();
|
||||||
if (!filePath.isEmpty()) {
|
if (!filePath.isEmpty()) {
|
||||||
ChatSerializer::saveToFile(m_chatModel, filePath);
|
m_historyStore->save(filePath);
|
||||||
setRecentFilePath(filePath);
|
setRecentFilePath(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::getAutosaveFilePath() const
|
QString ChatRootView::getAutosaveFilePath() const
|
||||||
{
|
{
|
||||||
if (!m_recentFilePath.isEmpty()) {
|
return m_historyStore->autosaveFilePath(m_recentFilePath);
|
||||||
return m_recentFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString dir = getChatsHistoryDir();
|
|
||||||
if (dir.isEmpty()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return QDir(dir).filePath(getSuggestedFileName() + ".json");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::getAutosaveFilePath(
|
QString ChatRootView::getAutosaveFilePath(
|
||||||
const QString &firstMessage, const QStringList &attachments) const
|
const QString &firstMessage, const QStringList &attachments) const
|
||||||
{
|
{
|
||||||
if (!m_recentFilePath.isEmpty()) {
|
return m_historyStore
|
||||||
return m_recentFilePath;
|
->autosaveFilePath(m_recentFilePath, firstMessage, hasImageAttachments(attachments));
|
||||||
}
|
|
||||||
|
|
||||||
QString dir = getChatsHistoryDir();
|
|
||||||
if (dir.isEmpty()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString shortMessage = firstMessage.split('\n').first().simplified().left(30);
|
|
||||||
|
|
||||||
if (shortMessage.isEmpty() && hasImageAttachments(attachments)) {
|
|
||||||
shortMessage = "image_chat";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString fileName = generateChatFileName(shortMessage, dir);
|
|
||||||
return QDir(dir).filePath(fileName + ".json");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList ChatRootView::attachmentFiles() const
|
QStringList ChatRootView::attachmentFiles() const
|
||||||
@@ -779,8 +615,7 @@ QStringList ChatRootView::convertUrlsToLocalPaths(const QVariantList &urls) cons
|
|||||||
|
|
||||||
void ChatRootView::calculateMessageTokensCount(const QString &message)
|
void ChatRootView::calculateMessageTokensCount(const QString &message)
|
||||||
{
|
{
|
||||||
m_messageTokensCount = Context::TokenUtils::estimateTokens(message);
|
m_tokenCounter->setMessage(message);
|
||||||
updateInputTokensCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::setIsSyncOpenFiles(bool state)
|
void ChatRootView::setIsSyncOpenFiles(bool state)
|
||||||
@@ -799,22 +634,7 @@ void ChatRootView::setIsSyncOpenFiles(bool state)
|
|||||||
|
|
||||||
void ChatRootView::openChatHistoryFolder()
|
void ChatRootView::openChatHistoryFolder()
|
||||||
{
|
{
|
||||||
QString path;
|
m_historyStore->openHistoryFolder();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::openRulesFolder()
|
void ChatRootView::openRulesFolder()
|
||||||
@@ -851,93 +671,14 @@ void ChatRootView::openFileInEditor(const QString &filePath)
|
|||||||
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
|
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::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,
|
|
||||||
&ChatRootView::updateInputTokensCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ChatRootView::updateInputTokensCount()
|
void ChatRootView::updateInputTokensCount()
|
||||||
{
|
{
|
||||||
int inputTokens = m_messageTokensCount;
|
m_tokenCounter->recompute();
|
||||||
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_attachmentFiles.isEmpty()) {
|
|
||||||
QStringList textPaths;
|
|
||||||
inputTokens += splitImageEstimate(m_attachmentFiles, textPaths);
|
|
||||||
if (!textPaths.isEmpty()) {
|
|
||||||
auto attachFiles = m_clientInterface->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_clientInterface->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 (useTools()) {
|
|
||||||
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_inputTokensCount = static_cast<int>(inputTokens * m_calibrationFactor);
|
|
||||||
emit inputTokensCountChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatRootView::inputTokensCount() const
|
int ChatRootView::inputTokensCount() const
|
||||||
{
|
{
|
||||||
return m_inputTokensCount;
|
return m_tokenCounter->inputTokens();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRootView::isSyncOpenFiles() const
|
bool ChatRootView::isSyncOpenFiles() const
|
||||||
@@ -1126,309 +867,57 @@ void ChatRootView::setUseThinking(bool enabled)
|
|||||||
|
|
||||||
void ChatRootView::applyFileEdit(const QString &editId)
|
void ChatRootView::applyFileEdit(const QString &editId)
|
||||||
{
|
{
|
||||||
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
m_fileEditController->applyFileEdit(editId);
|
||||||
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
|
||||||
m_lastInfoMessage = QString("File edit applied successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
updateFileEditStatus(editId, "applied");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to apply file edit")
|
|
||||||
: QString("Failed to apply file edit: %1").arg(edit.statusMessage);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::rejectFileEdit(const QString &editId)
|
void ChatRootView::rejectFileEdit(const QString &editId)
|
||||||
{
|
{
|
||||||
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
m_fileEditController->rejectFileEdit(editId);
|
||||||
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
|
||||||
m_lastInfoMessage = QString("File edit rejected");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to reject file edit")
|
|
||||||
: QString("Failed to reject file edit: %1").arg(edit.statusMessage);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::undoFileEdit(const QString &editId)
|
void ChatRootView::undoFileEdit(const QString &editId)
|
||||||
{
|
{
|
||||||
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
m_fileEditController->undoFileEdit(editId);
|
||||||
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
|
||||||
m_lastInfoMessage = QString("File edit undone successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
updateFileEditStatus(editId, "rejected");
|
|
||||||
} else {
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
m_lastErrorMessage = edit.statusMessage.isEmpty()
|
|
||||||
? QString("Failed to undo file edit")
|
|
||||||
: QString("Failed to undo file edit: %1").arg(edit.statusMessage);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::openFileEditInEditor(const QString &editId)
|
void ChatRootView::openFileEditInEditor(const QString &editId)
|
||||||
{
|
{
|
||||||
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
|
m_fileEditController->openFileEditInEditor(editId);
|
||||||
|
|
||||||
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
||||||
if (edit.editId.isEmpty()) {
|
|
||||||
m_lastErrorMessage = QString("File edit not found: %1").arg(editId);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
|
|
||||||
|
|
||||||
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
|
|
||||||
if (!editor) {
|
|
||||||
m_lastErrorMessage = QString("Failed to open file in editor: %1").arg(edit.filePath);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
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 ChatRootView::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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::applyAllFileEditsForCurrentMessage()
|
void ChatRootView::applyAllFileEditsForCurrentMessage()
|
||||||
{
|
{
|
||||||
if (m_currentMessageRequestId.isEmpty()) {
|
m_fileEditController->applyAllForCurrentMessage();
|
||||||
m_lastErrorMessage = QString("No active message with file edits");
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentMessageRequestId));
|
|
||||||
|
|
||||||
QString errorMsg;
|
|
||||||
bool success = Context::ChangesManager::instance()
|
|
||||||
.reapplyAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
m_lastInfoMessage = QString("All file edits applied successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
|
||||||
m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_lastErrorMessage = errorMsg.isEmpty()
|
|
||||||
? QString("Failed to apply some file edits")
|
|
||||||
: QString("Failed to apply some file edits:\n%1").arg(errorMsg);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
|
||||||
m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Applied) {
|
|
||||||
updateFileEditStatus(edit.editId, "applied");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::undoAllFileEditsForCurrentMessage()
|
void ChatRootView::undoAllFileEditsForCurrentMessage()
|
||||||
{
|
{
|
||||||
if (m_currentMessageRequestId.isEmpty()) {
|
m_fileEditController->undoAllForCurrentMessage();
|
||||||
m_lastErrorMessage = QString("No active message with file edits");
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentMessageRequestId));
|
|
||||||
|
|
||||||
QString errorMsg;
|
|
||||||
bool success = Context::ChangesManager::instance()
|
|
||||||
.undoAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
m_lastInfoMessage = QString("All file edits undone successfully");
|
|
||||||
emit lastInfoMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
|
||||||
m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m_lastErrorMessage = errorMsg.isEmpty()
|
|
||||||
? QString("Failed to undo some file edits")
|
|
||||||
: QString("Failed to undo some file edits:\n%1").arg(errorMsg);
|
|
||||||
emit lastErrorMessageChanged();
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(
|
|
||||||
m_currentMessageRequestId);
|
|
||||||
for (const auto &edit : edits) {
|
|
||||||
if (edit.status == Context::ChangesManager::Rejected) {
|
|
||||||
updateFileEditStatus(edit.editId, "rejected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCurrentMessageEditsStats();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::updateCurrentMessageEditsStats()
|
void ChatRootView::updateCurrentMessageEditsStats()
|
||||||
{
|
{
|
||||||
if (m_currentMessageRequestId.isEmpty()) {
|
m_fileEditController->updateStats();
|
||||||
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0
|
|
||||||
|| m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
|
|
||||||
m_currentMessageTotalEdits = 0;
|
|
||||||
m_currentMessageAppliedEdits = 0;
|
|
||||||
m_currentMessagePendingEdits = 0;
|
|
||||||
m_currentMessageRejectedEdits = 0;
|
|
||||||
emit currentMessageEditsStatsChanged();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
|
||||||
|
|
||||||
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_currentMessageTotalEdits != total) {
|
|
||||||
m_currentMessageTotalEdits = total;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_currentMessageAppliedEdits != applied) {
|
|
||||||
m_currentMessageAppliedEdits = applied;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_currentMessagePendingEdits != pending) {
|
|
||||||
m_currentMessagePendingEdits = pending;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (m_currentMessageRejectedEdits != rejected) {
|
|
||||||
m_currentMessageRejectedEdits = 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 currentMessageEditsStatsChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatRootView::currentMessageTotalEdits() const
|
int ChatRootView::currentMessageTotalEdits() const
|
||||||
{
|
{
|
||||||
return m_currentMessageTotalEdits;
|
return m_fileEditController->totalEdits();
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatRootView::currentMessageAppliedEdits() const
|
int ChatRootView::currentMessageAppliedEdits() const
|
||||||
{
|
{
|
||||||
return m_currentMessageAppliedEdits;
|
return m_fileEditController->appliedEdits();
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatRootView::currentMessagePendingEdits() const
|
int ChatRootView::currentMessagePendingEdits() const
|
||||||
{
|
{
|
||||||
return m_currentMessagePendingEdits;
|
return m_fileEditController->pendingEdits();
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatRootView::currentMessageRejectedEdits() const
|
int ChatRootView::currentMessageRejectedEdits() const
|
||||||
{
|
{
|
||||||
return m_currentMessageRejectedEdits;
|
return m_fileEditController->rejectedEdits();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::lastInfoMessage() const
|
QString ChatRootView::lastInfoMessage() const
|
||||||
@@ -1444,45 +933,6 @@ bool ChatRootView::isThinkingSupport() const
|
|||||||
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::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;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
||||||
{
|
{
|
||||||
for (const QString &filePath : attachments) {
|
for (const QString &filePath : attachments) {
|
||||||
@@ -1503,66 +953,22 @@ bool ChatRootView::isImageFile(const QString &filePath) const
|
|||||||
|
|
||||||
void ChatRootView::loadAvailableConfigurations()
|
void ChatRootView::loadAvailableConfigurations()
|
||||||
{
|
{
|
||||||
auto &manager = Settings::ConfigurationManager::instance();
|
m_configurationController->loadAvailableConfigurations();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &settings = Settings::generalSettings();
|
|
||||||
QString currentProvider = settings.caProvider.value();
|
|
||||||
QString currentModel = settings.caModel.value();
|
|
||||||
m_currentConfiguration = QString("%1 - %2").arg(currentProvider, currentModel);
|
|
||||||
|
|
||||||
emit availableConfigurationsChanged();
|
|
||||||
emit currentConfigurationChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::applyConfiguration(const QString &configName)
|
void ChatRootView::applyConfiguration(const QString &configName)
|
||||||
{
|
{
|
||||||
if (configName == QObject::tr("Current Settings")) {
|
m_configurationController->applyConfiguration(configName);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList ChatRootView::availableConfigurations() const
|
QStringList ChatRootView::availableConfigurations() const
|
||||||
{
|
{
|
||||||
return m_availableConfigurations;
|
return m_configurationController->availableConfigurations();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ChatRootView::currentConfiguration() const
|
QString ChatRootView::currentConfiguration() const
|
||||||
{
|
{
|
||||||
return m_currentConfiguration;
|
return m_configurationController->currentConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::loadAvailableAgentRoles()
|
void ChatRootView::loadAvailableAgentRoles()
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ namespace QodeAssist::Chat {
|
|||||||
|
|
||||||
class ChatCompressor;
|
class ChatCompressor;
|
||||||
class AgentRoleController;
|
class AgentRoleController;
|
||||||
|
class ChatConfigurationController;
|
||||||
|
class FileEditController;
|
||||||
|
class InputTokenCounter;
|
||||||
|
class ChatHistoryStore;
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
class ChatRootView : public QQuickItem
|
||||||
{
|
{
|
||||||
@@ -213,9 +217,6 @@ signals:
|
|||||||
void openFilesChanged();
|
void openFilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void rewireToolsChangedConnection();
|
|
||||||
QMetaObject::Connection m_toolsChangedConn;
|
|
||||||
|
|
||||||
bool deferSendForAutoCompress(
|
bool deferSendForAutoCompress(
|
||||||
const QString &message,
|
const QString &message,
|
||||||
const QStringList &attachments,
|
const QStringList &attachments,
|
||||||
@@ -228,10 +229,6 @@ private:
|
|||||||
const QStringList &linkedFiles,
|
const QStringList &linkedFiles,
|
||||||
bool useTools,
|
bool useTools,
|
||||||
bool useThinking);
|
bool useThinking);
|
||||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
|
||||||
QString getChatsHistoryDir() const;
|
|
||||||
QString getSuggestedFileName() const;
|
|
||||||
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
|
||||||
bool hasImageAttachments(const QStringList &attachments) const;
|
bool hasImageAttachments(const QStringList &attachments) const;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
@@ -242,10 +239,6 @@ private:
|
|||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
QStringList m_linkedFiles;
|
QStringList m_linkedFiles;
|
||||||
int m_messageTokensCount{0};
|
|
||||||
int m_inputTokensCount{0};
|
|
||||||
int m_lastSentEstimate{0};
|
|
||||||
double m_calibrationFactor{1.0};
|
|
||||||
|
|
||||||
struct PendingSend {
|
struct PendingSend {
|
||||||
QString message;
|
QString message;
|
||||||
@@ -262,18 +255,14 @@ private:
|
|||||||
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;
|
|
||||||
|
|
||||||
ChatCompressor *m_chatCompressor;
|
ChatCompressor *m_chatCompressor;
|
||||||
AgentRoleController *m_agentRoleController;
|
AgentRoleController *m_agentRoleController;
|
||||||
|
ChatConfigurationController *m_configurationController;
|
||||||
|
FileEditController *m_fileEditController;
|
||||||
|
InputTokenCounter *m_tokenCounter;
|
||||||
|
ChatHistoryStore *m_historyStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
334
ChatView/FileEditController.cpp
Normal file
334
ChatView/FileEditController.cpp
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "FileEditController.hpp"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
|
||||||
|
#include "ChatModel.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
FileEditController::FileEditController(ChatModel *chatModel, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_chatModel(chatModel)
|
||||||
|
{
|
||||||
|
auto &changes = Context::ChangesManager::instance();
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditAdded, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditApplied, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditRejected, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditUndone, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
connect(&changes, &Context::ChangesManager::fileEditArchived, this, [this](const QString &) {
|
||||||
|
updateStats();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::setCurrentRequestId(const QString &requestId)
|
||||||
|
{
|
||||||
|
if (!m_currentRequestId.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentRequestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentRequestId = requestId;
|
||||||
|
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::clearCurrentRequestId()
|
||||||
|
{
|
||||||
|
m_currentRequestId.clear();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::totalEdits() const
|
||||||
|
{
|
||||||
|
return m_totalEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::appliedEdits() const
|
||||||
|
{
|
||||||
|
return m_appliedEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::pendingEdits() const
|
||||||
|
{
|
||||||
|
return m_pendingEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileEditController::rejectedEdits() const
|
||||||
|
{
|
||||||
|
return m_rejectedEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::applyFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
|
||||||
|
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
|
||||||
|
emit infoMessage(QString("File edit applied successfully"));
|
||||||
|
updateFileEditStatus(editId, "applied");
|
||||||
|
} else {
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
emit errorOccurred(
|
||||||
|
edit.statusMessage.isEmpty()
|
||||||
|
? QString("Failed to apply file edit")
|
||||||
|
: QString("Failed to apply file edit: %1").arg(edit.statusMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::rejectFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
|
||||||
|
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
|
||||||
|
emit infoMessage(QString("File edit rejected"));
|
||||||
|
updateFileEditStatus(editId, "rejected");
|
||||||
|
} else {
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
emit errorOccurred(
|
||||||
|
edit.statusMessage.isEmpty()
|
||||||
|
? QString("Failed to reject file edit")
|
||||||
|
: QString("Failed to reject file edit: %1").arg(edit.statusMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::undoFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
|
||||||
|
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
|
||||||
|
emit infoMessage(QString("File edit undone successfully"));
|
||||||
|
updateFileEditStatus(editId, "rejected");
|
||||||
|
} else {
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
emit errorOccurred(
|
||||||
|
edit.statusMessage.isEmpty()
|
||||||
|
? QString("Failed to undo file edit")
|
||||||
|
: QString("Failed to undo file edit: %1").arg(edit.statusMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::openFileEditInEditor(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
|
||||||
|
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
if (edit.editId.isEmpty()) {
|
||||||
|
emit errorOccurred(QString("File edit not found: %1").arg(editId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
|
||||||
|
|
||||||
|
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
|
||||||
|
if (!editor) {
|
||||||
|
emit errorOccurred(QString("Failed to open file in editor: %1").arg(edit.filePath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
|
||||||
|
if (textEditor && textEditor->editorWidget()) {
|
||||||
|
QTextDocument *doc = textEditor->editorWidget()->document();
|
||||||
|
if (doc) {
|
||||||
|
QString currentContent = doc->toPlainText();
|
||||||
|
int position = -1;
|
||||||
|
|
||||||
|
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
||||||
|
position = currentContent.indexOf(edit.newContent);
|
||||||
|
} else if (!edit.oldContent.isEmpty()) {
|
||||||
|
position = currentContent.indexOf(edit.oldContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position >= 0) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
textEditor->editorWidget()->setTextCursor(cursor);
|
||||||
|
textEditor->editorWidget()->centerCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::updateFileEditStatus(const QString &editId, const QString &status)
|
||||||
|
{
|
||||||
|
auto messages = m_chatModel->getChatHistory();
|
||||||
|
for (int i = 0; i < messages.size(); ++i) {
|
||||||
|
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
|
||||||
|
QString content = messages[i].content;
|
||||||
|
|
||||||
|
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||||
|
int markerPos = content.indexOf(marker);
|
||||||
|
|
||||||
|
QString jsonStr = content;
|
||||||
|
if (markerPos >= 0) {
|
||||||
|
jsonStr = content.mid(markerPos + marker.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||||
|
if (doc.isObject()) {
|
||||||
|
QJsonObject obj = doc.object();
|
||||||
|
obj["status"] = status;
|
||||||
|
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
if (!edit.statusMessage.isEmpty()) {
|
||||||
|
obj["status_message"] = edit.statusMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString updatedContent = marker
|
||||||
|
+ QString::fromUtf8(
|
||||||
|
QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||||
|
m_chatModel->updateMessageContent(editId, updatedContent);
|
||||||
|
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::applyAllForCurrentMessage()
|
||||||
|
{
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
emit errorOccurred(QString("No active message with file edits"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentRequestId));
|
||||||
|
|
||||||
|
QString errorMsg;
|
||||||
|
bool success = Context::ChangesManager::instance()
|
||||||
|
.reapplyAllEditsForRequest(m_currentRequestId, &errorMsg);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
emit infoMessage(QString("All file edits applied successfully"));
|
||||||
|
} else {
|
||||||
|
emit errorOccurred(
|
||||||
|
errorMsg.isEmpty()
|
||||||
|
? QString("Failed to apply some file edits")
|
||||||
|
: QString("Failed to apply some file edits:\n%1").arg(errorMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||||
|
for (const auto &edit : edits) {
|
||||||
|
if (edit.status == Context::ChangesManager::Applied) {
|
||||||
|
updateFileEditStatus(edit.editId, "applied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::undoAllForCurrentMessage()
|
||||||
|
{
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
emit errorOccurred(QString("No active message with file edits"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentRequestId));
|
||||||
|
|
||||||
|
QString errorMsg;
|
||||||
|
bool success = Context::ChangesManager::instance()
|
||||||
|
.undoAllEditsForRequest(m_currentRequestId, &errorMsg);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
emit infoMessage(QString("All file edits undone successfully"));
|
||||||
|
} else {
|
||||||
|
emit errorOccurred(
|
||||||
|
errorMsg.isEmpty()
|
||||||
|
? QString("Failed to undo some file edits")
|
||||||
|
: QString("Failed to undo some file edits:\n%1").arg(errorMsg));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||||
|
for (const auto &edit : edits) {
|
||||||
|
if (edit.status == Context::ChangesManager::Rejected) {
|
||||||
|
updateFileEditStatus(edit.editId, "rejected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileEditController::updateStats()
|
||||||
|
{
|
||||||
|
if (m_currentRequestId.isEmpty()) {
|
||||||
|
if (m_totalEdits != 0 || m_appliedEdits != 0 || m_pendingEdits != 0
|
||||||
|
|| m_rejectedEdits != 0) {
|
||||||
|
m_totalEdits = 0;
|
||||||
|
m_appliedEdits = 0;
|
||||||
|
m_pendingEdits = 0;
|
||||||
|
m_rejectedEdits = 0;
|
||||||
|
emit statsChanged();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentRequestId);
|
||||||
|
|
||||||
|
int total = edits.size();
|
||||||
|
int applied = 0;
|
||||||
|
int pending = 0;
|
||||||
|
int rejected = 0;
|
||||||
|
|
||||||
|
for (const auto &edit : edits) {
|
||||||
|
switch (edit.status) {
|
||||||
|
case Context::ChangesManager::Applied:
|
||||||
|
applied++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Pending:
|
||||||
|
pending++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Rejected:
|
||||||
|
rejected++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Archived:
|
||||||
|
total--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (m_totalEdits != total) {
|
||||||
|
m_totalEdits = total;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_appliedEdits != applied) {
|
||||||
|
m_appliedEdits = applied;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_pendingEdits != pending) {
|
||||||
|
m_pendingEdits = pending;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_rejectedEdits != rejected) {
|
||||||
|
m_rejectedEdits = rejected;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
|
||||||
|
.arg(total)
|
||||||
|
.arg(applied)
|
||||||
|
.arg(pending)
|
||||||
|
.arg(rejected));
|
||||||
|
emit statsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
53
ChatView/FileEditController.hpp
Normal file
53
ChatView/FileEditController.hpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatModel;
|
||||||
|
|
||||||
|
class FileEditController : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FileEditController(ChatModel *chatModel, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setCurrentRequestId(const QString &requestId);
|
||||||
|
void clearCurrentRequestId();
|
||||||
|
|
||||||
|
int totalEdits() const;
|
||||||
|
int appliedEdits() const;
|
||||||
|
int pendingEdits() const;
|
||||||
|
int rejectedEdits() const;
|
||||||
|
|
||||||
|
void applyFileEdit(const QString &editId);
|
||||||
|
void rejectFileEdit(const QString &editId);
|
||||||
|
void undoFileEdit(const QString &editId);
|
||||||
|
void openFileEditInEditor(const QString &editId);
|
||||||
|
|
||||||
|
void applyAllForCurrentMessage();
|
||||||
|
void undoAllForCurrentMessage();
|
||||||
|
void updateStats();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void statsChanged();
|
||||||
|
void infoMessage(const QString &message);
|
||||||
|
void errorOccurred(const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
|
|
||||||
|
ChatModel *m_chatModel;
|
||||||
|
QString m_currentRequestId;
|
||||||
|
int m_totalEdits{0};
|
||||||
|
int m_appliedEdits{0};
|
||||||
|
int m_pendingEdits{0};
|
||||||
|
int m_rejectedEdits{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
183
ChatView/InputTokenCounter.cpp
Normal file
183
ChatView/InputTokenCounter.cpp
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "InputTokenCounter.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <LLMQore/ToolsManager.hpp>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
|
||||||
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "ChatModel.hpp"
|
||||||
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "ProvidersManager.hpp"
|
||||||
|
#include "context/ContextManager.hpp"
|
||||||
|
#include "context/TokenUtils.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
InputTokenCounter::InputTokenCounter(
|
||||||
|
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_chatModel(chatModel)
|
||||||
|
, m_contextManager(contextManager)
|
||||||
|
{
|
||||||
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
connect(
|
||||||
|
&settings.useSystemPrompt,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&InputTokenCounter::recompute);
|
||||||
|
connect(
|
||||||
|
&settings.systemPrompt, &Utils::BaseAspect::changed, this, &InputTokenCounter::recompute);
|
||||||
|
connect(
|
||||||
|
&settings.enableChatTools,
|
||||||
|
&Utils::BaseAspect::changed,
|
||||||
|
this,
|
||||||
|
&InputTokenCounter::recompute);
|
||||||
|
|
||||||
|
connect(&Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, this, [this]() {
|
||||||
|
rewireToolsChangedConnection();
|
||||||
|
recompute();
|
||||||
|
});
|
||||||
|
|
||||||
|
rewireToolsChangedConnection();
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
int InputTokenCounter::inputTokens() const
|
||||||
|
{
|
||||||
|
return m_inputTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::setMessage(const QString &message)
|
||||||
|
{
|
||||||
|
m_messageTokens = Context::TokenUtils::estimateTokens(message);
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::setAttachments(const QStringList &attachments)
|
||||||
|
{
|
||||||
|
m_attachments = attachments;
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::setLinkedFiles(const QStringList &linkedFiles)
|
||||||
|
{
|
||||||
|
m_linkedFiles = linkedFiles;
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::rewireToolsChangedConnection()
|
||||||
|
{
|
||||||
|
if (m_toolsChangedConn)
|
||||||
|
QObject::disconnect(m_toolsChangedConn);
|
||||||
|
m_toolsChangedConn = {};
|
||||||
|
|
||||||
|
const auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
if (!provider)
|
||||||
|
return;
|
||||||
|
auto *tm = provider->toolsManager();
|
||||||
|
if (!tm)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_toolsChangedConn = connect(
|
||||||
|
tm, &::LLMQore::ToolRegistry::toolsChanged, this, &InputTokenCounter::recompute);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::recompute()
|
||||||
|
{
|
||||||
|
int inputTokens = m_messageTokens;
|
||||||
|
auto &settings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
|
if (settings.useSystemPrompt()) {
|
||||||
|
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto splitImageEstimate = [](const QStringList &paths, QStringList &textPaths) {
|
||||||
|
int imageTokens = 0;
|
||||||
|
for (const QString &p : paths) {
|
||||||
|
if (Context::TokenUtils::isImageFilePath(p))
|
||||||
|
imageTokens += Context::TokenUtils::estimateImageAttachmentTokens(p);
|
||||||
|
else
|
||||||
|
textPaths.append(p);
|
||||||
|
}
|
||||||
|
return imageTokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!m_attachments.isEmpty()) {
|
||||||
|
QStringList textPaths;
|
||||||
|
inputTokens += splitImageEstimate(m_attachments, textPaths);
|
||||||
|
if (!textPaths.isEmpty()) {
|
||||||
|
auto attachFiles = m_contextManager->getContentFiles(textPaths);
|
||||||
|
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_linkedFiles.isEmpty()) {
|
||||||
|
QStringList textPaths;
|
||||||
|
inputTokens += splitImageEstimate(m_linkedFiles, textPaths);
|
||||||
|
if (!textPaths.isEmpty()) {
|
||||||
|
auto linkFiles = m_contextManager->getContentFiles(textPaths);
|
||||||
|
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &history = m_chatModel->getChatHistory();
|
||||||
|
for (const auto &message : history) {
|
||||||
|
inputTokens += Context::TokenUtils::estimateTokens(message.content);
|
||||||
|
inputTokens += 4; // + role
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.enableChatTools()) {
|
||||||
|
const auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
if (auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(
|
||||||
|
providerName)) {
|
||||||
|
if (auto *tm = provider->toolsManager()) {
|
||||||
|
const QJsonArray toolDefs = tm->getToolsDefinitions();
|
||||||
|
if (!toolDefs.isEmpty()) {
|
||||||
|
const QByteArray serialized
|
||||||
|
= QJsonDocument(toolDefs).toJson(QJsonDocument::Compact);
|
||||||
|
inputTokens += static_cast<int>(serialized.size() / 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_inputTokens = static_cast<int>(inputTokens * m_calibrationFactor);
|
||||||
|
emit inputTokensChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::recordSent()
|
||||||
|
{
|
||||||
|
m_lastSentEstimate = m_calibrationFactor > 0.0
|
||||||
|
? static_cast<int>(m_inputTokens / m_calibrationFactor)
|
||||||
|
: m_inputTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputTokenCounter::recordServerUsage(int promptTokens)
|
||||||
|
{
|
||||||
|
if (promptTokens <= 0 || m_lastSentEstimate <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const double rawFactor
|
||||||
|
= static_cast<double>(promptTokens) / static_cast<double>(m_lastSentEstimate);
|
||||||
|
const double clamped = std::clamp(rawFactor, 0.5, 3.0);
|
||||||
|
m_calibrationFactor = 0.5 * m_calibrationFactor + 0.5 * clamped;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Token calibration: server=%1 estimated=%2 ratio=%3 ema=%4")
|
||||||
|
.arg(promptTokens)
|
||||||
|
.arg(m_lastSentEstimate)
|
||||||
|
.arg(rawFactor, 0, 'f', 3)
|
||||||
|
.arg(m_calibrationFactor, 0, 'f', 3));
|
||||||
|
|
||||||
|
recompute();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
53
ChatView/InputTokenCounter.hpp
Normal file
53
ChatView/InputTokenCounter.hpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace QodeAssist::Context {
|
||||||
|
class ContextManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatModel;
|
||||||
|
|
||||||
|
class InputTokenCounter : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
InputTokenCounter(
|
||||||
|
ChatModel *chatModel, Context::ContextManager *contextManager, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int inputTokens() const;
|
||||||
|
|
||||||
|
void setMessage(const QString &message);
|
||||||
|
void setAttachments(const QStringList &attachments);
|
||||||
|
void setLinkedFiles(const QStringList &linkedFiles);
|
||||||
|
void recompute();
|
||||||
|
|
||||||
|
void recordSent();
|
||||||
|
void recordServerUsage(int promptTokens);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void inputTokensChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rewireToolsChangedConnection();
|
||||||
|
|
||||||
|
ChatModel *m_chatModel;
|
||||||
|
Context::ContextManager *m_contextManager;
|
||||||
|
QMetaObject::Connection m_toolsChangedConn;
|
||||||
|
|
||||||
|
QStringList m_attachments;
|
||||||
|
QStringList m_linkedFiles;
|
||||||
|
int m_messageTokens{0};
|
||||||
|
int m_inputTokens{0};
|
||||||
|
int m_lastSentEstimate{0};
|
||||||
|
double m_calibrationFactor{1.0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
Reference in New Issue
Block a user