mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 02:49:12 -04:00
1044 lines
28 KiB
C++
1044 lines
28 KiB
C++
// Copyright (C) 2024-2026 Petr Mironychev
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "ChatRootView.hpp"
|
|
|
|
#include <QClipboard>
|
|
#include <QDesktopServices>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileDialog>
|
|
#include <QFileInfo>
|
|
#include <QMessageBox>
|
|
#include <QTextStream>
|
|
|
|
#include <coreplugin/editormanager/editormanager.h>
|
|
#include <projectexplorer/project.h>
|
|
#include <projectexplorer/projectexplorer.h>
|
|
#include <projectexplorer/projectmanager.h>
|
|
#include <utils/theme/theme.h>
|
|
#include <utils/utilsicons.h>
|
|
|
|
#include "AgentRoleController.hpp"
|
|
#include "ChatAssistantSettings.hpp"
|
|
#include "ChatConfigurationController.hpp"
|
|
#include "ChatCompressor.hpp"
|
|
#include "ChatHistoryStore.hpp"
|
|
#include "FileEditController.hpp"
|
|
#include "GeneralSettings.hpp"
|
|
#include "InputTokenCounter.hpp"
|
|
#include "SettingsConstants.hpp"
|
|
#include "Logger.hpp"
|
|
#include "ProvidersManager.hpp"
|
|
#include "context/ContextManager.hpp"
|
|
#include "pluginllmcore/RulesLoader.hpp"
|
|
|
|
namespace QodeAssist::Chat {
|
|
|
|
ChatRootView::ChatRootView(QQuickItem *parent)
|
|
: QQuickItem(parent)
|
|
, m_chatModel(new ChatModel(this))
|
|
, m_promptProvider(PluginLLMCore::PromptTemplateManager::instance())
|
|
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
|
|
, m_fileManager(new ChatFileManager(this))
|
|
, m_isRequestInProgress(false)
|
|
, m_chatCompressor(new ChatCompressor(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();
|
|
connect(
|
|
&Settings::chatAssistantSettings().linkOpenFiles,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); });
|
|
|
|
auto &settings = Settings::generalSettings();
|
|
|
|
connect(
|
|
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
|
|
|
|
connect(
|
|
m_configurationController,
|
|
&ChatConfigurationController::availableConfigurationsChanged,
|
|
this,
|
|
&ChatRootView::availableConfigurationsChanged);
|
|
connect(
|
|
m_configurationController,
|
|
&ChatConfigurationController::currentConfigurationChanged,
|
|
this,
|
|
&ChatRootView::currentConfigurationChanged);
|
|
|
|
connect(
|
|
m_clientInterface,
|
|
&ClientInterface::messageReceivedCompletely,
|
|
this,
|
|
&ChatRootView::autosave);
|
|
|
|
connect(m_clientInterface, &ClientInterface::messageReceivedCompletely, this, [this]() {
|
|
this->setRequestProgressStatus(false);
|
|
});
|
|
|
|
connect(
|
|
m_clientInterface,
|
|
&ClientInterface::messageReceivedCompletely,
|
|
this,
|
|
&ChatRootView::updateInputTokensCount);
|
|
|
|
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
|
setRecentFilePath(QString{});
|
|
m_fileEditController->clearCurrentRequestId();
|
|
});
|
|
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::useToolsChanged, this, &ChatRootView::updateInputTokensCount);
|
|
|
|
connect(
|
|
m_tokenCounter,
|
|
&InputTokenCounter::inputTokensChanged,
|
|
this,
|
|
&ChatRootView::inputTokensCountChanged);
|
|
connect(
|
|
m_agentRoleController,
|
|
&AgentRoleController::availableRolesChanged,
|
|
this,
|
|
&ChatRootView::availableAgentRolesChanged);
|
|
connect(
|
|
m_agentRoleController,
|
|
&AgentRoleController::currentRoleChanged,
|
|
this,
|
|
&ChatRootView::currentAgentRoleChanged);
|
|
connect(
|
|
m_agentRoleController,
|
|
&AgentRoleController::baseSystemPromptChanged,
|
|
this,
|
|
&ChatRootView::baseSystemPromptChanged);
|
|
|
|
auto editors = Core::EditorManager::instance();
|
|
|
|
connect(editors, &Core::EditorManager::editorCreated, this, &ChatRootView::onEditorCreated);
|
|
connect(
|
|
editors,
|
|
&Core::EditorManager::editorAboutToClose,
|
|
this,
|
|
&ChatRootView::onEditorAboutToClose);
|
|
|
|
connect(editors, &Core::EditorManager::currentEditorAboutToChange, this, [this]() {
|
|
if (m_isSyncOpenFiles) {
|
|
for (auto editor : std::as_const(m_currentEditors)) {
|
|
onAppendLinkFileFromEditor(editor);
|
|
}
|
|
}
|
|
});
|
|
connect(
|
|
&Settings::chatAssistantSettings().textFontFamily,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::textFamilyChanged);
|
|
connect(
|
|
&Settings::chatAssistantSettings().codeFontFamily,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::codeFamilyChanged);
|
|
connect(
|
|
&Settings::chatAssistantSettings().textFontSize,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::textFontSizeChanged);
|
|
connect(
|
|
&Settings::chatAssistantSettings().codeFontSize,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::codeFontSizeChanged);
|
|
connect(
|
|
&Settings::chatAssistantSettings().textFormat,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::textFormatChanged);
|
|
connect(m_clientInterface, &ClientInterface::errorOccurred, this, [this](const QString &error) {
|
|
this->setRequestProgressStatus(false);
|
|
m_lastErrorMessage = error;
|
|
emit lastErrorMessageChanged();
|
|
});
|
|
|
|
connect(
|
|
m_clientInterface,
|
|
&ClientInterface::requestStarted,
|
|
this,
|
|
[this](const QString &requestId) { m_fileEditController->setCurrentRequestId(requestId); });
|
|
|
|
connect(
|
|
m_clientInterface,
|
|
&ClientInterface::messageUsageReceived,
|
|
this,
|
|
[this](int promptTokens, int /*completionTokens*/, int /*cached*/, int /*reasoning*/) {
|
|
m_tokenCounter->recordServerUsage(promptTokens);
|
|
});
|
|
|
|
connect(
|
|
m_fileEditController,
|
|
&FileEditController::statsChanged,
|
|
this,
|
|
&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(
|
|
m_historyStore, &ChatHistoryStore::saveRequested, this, &ChatRootView::saveHistory);
|
|
connect(
|
|
m_historyStore, &ChatHistoryStore::loadRequested, this, &ChatRootView::loadHistory);
|
|
|
|
refreshRules();
|
|
|
|
connect(
|
|
ProjectExplorer::ProjectManager::instance(),
|
|
&ProjectExplorer::ProjectManager::startupProjectChanged,
|
|
this,
|
|
&ChatRootView::refreshRules);
|
|
|
|
connect(
|
|
ProjectExplorer::ProjectManager::instance(),
|
|
&ProjectExplorer::ProjectManager::projectAdded,
|
|
this,
|
|
&ChatRootView::openFilesChanged);
|
|
|
|
connect(
|
|
ProjectExplorer::ProjectManager::instance(),
|
|
&ProjectExplorer::ProjectManager::projectRemoved,
|
|
this,
|
|
&ChatRootView::openFilesChanged);
|
|
|
|
connect(
|
|
&Settings::chatAssistantSettings().enableChatTools,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::useToolsChanged);
|
|
|
|
connect(
|
|
&Settings::chatAssistantSettings().enableThinkingMode,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::useThinkingChanged);
|
|
|
|
connect(
|
|
&Settings::generalSettings().caProvider,
|
|
&Utils::BaseAspect::changed,
|
|
this,
|
|
&ChatRootView::isThinkingSupportChanged);
|
|
|
|
connect(m_fileManager, &ChatFileManager::fileOperationFailed, this, [this](const QString &error) {
|
|
m_lastErrorMessage = error;
|
|
emit lastErrorMessageChanged();
|
|
});
|
|
|
|
connect(m_chatCompressor, &ChatCompressor::compressionStarted, this, [this]() {
|
|
emit isCompressingChanged();
|
|
});
|
|
|
|
connect(m_chatCompressor, &ChatCompressor::compressionCompleted, this, [this](const QString &compressedChatPath) {
|
|
emit isCompressingChanged();
|
|
m_lastInfoMessage = tr("Chat compressed successfully!");
|
|
emit lastInfoMessageChanged();
|
|
emit compressionCompleted(compressedChatPath);
|
|
|
|
loadHistory(compressedChatPath);
|
|
|
|
if (m_pendingSend.active) {
|
|
PendingSend p = m_pendingSend;
|
|
m_pendingSend = {};
|
|
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
|
}
|
|
});
|
|
|
|
connect(m_chatCompressor, &ChatCompressor::compressionFailed, this, [this](const QString &error) {
|
|
emit isCompressingChanged();
|
|
m_lastErrorMessage = error;
|
|
emit lastErrorMessageChanged();
|
|
emit compressionFailed(error);
|
|
|
|
if (m_pendingSend.active) {
|
|
PendingSend p = m_pendingSend;
|
|
m_pendingSend = {};
|
|
dispatchSend(p.message, p.attachments, p.linkedFiles, p.useTools, p.useThinking);
|
|
}
|
|
});
|
|
}
|
|
|
|
ChatModel *ChatRootView::chatModel() const
|
|
{
|
|
return m_chatModel;
|
|
}
|
|
|
|
void ChatRootView::sendMessage(const QString &message)
|
|
{
|
|
const QStringList attachments = m_attachmentFiles;
|
|
const QStringList linkedFiles = m_linkedFiles;
|
|
const bool tools = useTools();
|
|
const bool thinking = useThinking();
|
|
|
|
if (deferSendForAutoCompress(message, attachments, linkedFiles, tools, thinking))
|
|
return;
|
|
|
|
dispatchSend(message, attachments, linkedFiles, tools, thinking);
|
|
}
|
|
|
|
bool ChatRootView::deferSendForAutoCompress(
|
|
const QString &message,
|
|
const QStringList &attachments,
|
|
const QStringList &linkedFiles,
|
|
bool useToolsArg,
|
|
bool useThinkingArg)
|
|
{
|
|
auto &settings = Settings::chatAssistantSettings();
|
|
if (!settings.autoCompress())
|
|
return false;
|
|
|
|
const int threshold = settings.autoCompressThreshold();
|
|
const int inputTokens = m_tokenCounter->inputTokens();
|
|
if (inputTokens < threshold)
|
|
return false;
|
|
|
|
if (m_recentFilePath.isEmpty()) {
|
|
QString filePath = getAutosaveFilePath(message, attachments);
|
|
if (filePath.isEmpty())
|
|
return false;
|
|
setRecentFilePath(filePath);
|
|
LOG_MESSAGE(QString("Set chat file path for new chat (auto-compress): %1").arg(filePath));
|
|
}
|
|
|
|
if (m_chatCompressor->isCompressing() || m_pendingSend.active)
|
|
return false;
|
|
|
|
LOG_MESSAGE(QString("Auto-compress preempt: estimated next=%1 ≥ threshold=%2; deferring send")
|
|
.arg(inputTokens)
|
|
.arg(threshold));
|
|
|
|
m_pendingSend = {message, attachments, linkedFiles, useToolsArg, useThinkingArg, true};
|
|
compressCurrentChat();
|
|
return true;
|
|
}
|
|
|
|
void ChatRootView::dispatchSend(
|
|
const QString &message,
|
|
const QStringList &attachments,
|
|
const QStringList &linkedFiles,
|
|
bool useToolsArg,
|
|
bool useThinkingArg)
|
|
{
|
|
if (m_recentFilePath.isEmpty()) {
|
|
QString filePath = getAutosaveFilePath(message, attachments);
|
|
if (!filePath.isEmpty()) {
|
|
setRecentFilePath(filePath);
|
|
LOG_MESSAGE(QString("Set chat file path for new chat: %1").arg(filePath));
|
|
}
|
|
}
|
|
|
|
m_tokenCounter->recordSent();
|
|
|
|
m_clientInterface->sendMessage(message, attachments, linkedFiles, useToolsArg, useThinkingArg);
|
|
|
|
m_fileManager->clearIntermediateStorage();
|
|
clearAttachmentFiles();
|
|
setRequestProgressStatus(true);
|
|
}
|
|
|
|
void ChatRootView::copyToClipboard(const QString &text)
|
|
{
|
|
QGuiApplication::clipboard()->setText(text);
|
|
}
|
|
|
|
void ChatRootView::cancelRequest()
|
|
{
|
|
m_clientInterface->cancelRequest();
|
|
setRequestProgressStatus(false);
|
|
}
|
|
|
|
void ChatRootView::clearAttachmentFiles()
|
|
{
|
|
if (m_attachmentFiles.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
m_attachmentFiles.clear();
|
|
emit attachmentFilesChanged();
|
|
m_fileManager->clearIntermediateStorage();
|
|
}
|
|
|
|
void ChatRootView::clearLinkedFiles()
|
|
{
|
|
if (m_linkedFiles.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
m_linkedFiles.clear();
|
|
emit linkedFilesChanged();
|
|
}
|
|
|
|
void ChatRootView::clearMessages()
|
|
{
|
|
m_clientInterface->clearMessages();
|
|
clearLinkedFiles();
|
|
}
|
|
|
|
QString ChatRootView::currentTemplate() const
|
|
{
|
|
auto &settings = Settings::generalSettings();
|
|
return settings.caModel();
|
|
}
|
|
|
|
void ChatRootView::saveHistory(const QString &filePath)
|
|
{
|
|
auto result = m_historyStore->save(filePath);
|
|
if (!result.success) {
|
|
LOG_MESSAGE(QString("Failed to save chat history: %1").arg(result.errorMessage));
|
|
} else {
|
|
setRecentFilePath(filePath);
|
|
}
|
|
}
|
|
|
|
void ChatRootView::loadHistory(const QString &filePath)
|
|
{
|
|
auto result = m_historyStore->load(filePath);
|
|
if (!result.success) {
|
|
LOG_MESSAGE(QString("Failed to load chat history: %1").arg(result.errorMessage));
|
|
} else {
|
|
setRecentFilePath(filePath);
|
|
}
|
|
|
|
if (!m_pendingSend.active)
|
|
m_fileManager->clearIntermediateStorage();
|
|
m_attachmentFiles.clear();
|
|
m_linkedFiles.clear();
|
|
emit attachmentFilesChanged();
|
|
emit linkedFilesChanged();
|
|
|
|
m_fileEditController->clearCurrentRequestId();
|
|
updateInputTokensCount();
|
|
}
|
|
|
|
void ChatRootView::showSaveDialog()
|
|
{
|
|
m_historyStore->showSaveDialog();
|
|
}
|
|
|
|
void ChatRootView::showLoadDialog()
|
|
{
|
|
m_historyStore->showLoadDialog();
|
|
}
|
|
|
|
void ChatRootView::autosave()
|
|
{
|
|
if (m_chatModel->rowCount() == 0 || !Settings::chatAssistantSettings().autosave()) {
|
|
return;
|
|
}
|
|
|
|
QString filePath = getAutosaveFilePath();
|
|
if (!filePath.isEmpty()) {
|
|
m_historyStore->save(filePath);
|
|
setRecentFilePath(filePath);
|
|
}
|
|
}
|
|
|
|
QString ChatRootView::getAutosaveFilePath() const
|
|
{
|
|
return m_historyStore->autosaveFilePath(m_recentFilePath);
|
|
}
|
|
|
|
QString ChatRootView::getAutosaveFilePath(
|
|
const QString &firstMessage, const QStringList &attachments) const
|
|
{
|
|
return m_historyStore
|
|
->autosaveFilePath(m_recentFilePath, firstMessage, hasImageAttachments(attachments));
|
|
}
|
|
|
|
QStringList ChatRootView::attachmentFiles() const
|
|
{
|
|
return m_attachmentFiles;
|
|
}
|
|
|
|
QStringList ChatRootView::linkedFiles() const
|
|
{
|
|
return m_linkedFiles;
|
|
}
|
|
|
|
void ChatRootView::showAttachFilesDialog()
|
|
{
|
|
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
|
|
dialog.setFileMode(QFileDialog::ExistingFiles);
|
|
|
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
|
dialog.setDirectory(project->projectDirectory().toFSPathString());
|
|
}
|
|
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
addFilesToAttachList(dialog.selectedFiles());
|
|
}
|
|
}
|
|
|
|
void ChatRootView::addFilesToAttachList(const QStringList &filePaths)
|
|
{
|
|
if (filePaths.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const QStringList processedPaths = m_fileManager->processDroppedFiles(filePaths);
|
|
|
|
bool filesAdded = false;
|
|
for (const QString &filePath : processedPaths) {
|
|
if (!m_attachmentFiles.contains(filePath)) {
|
|
m_attachmentFiles.append(filePath);
|
|
filesAdded = true;
|
|
}
|
|
}
|
|
|
|
if (filesAdded) {
|
|
emit attachmentFilesChanged();
|
|
}
|
|
}
|
|
|
|
void ChatRootView::removeFileFromAttachList(int index)
|
|
{
|
|
if (index < 0 || index >= m_attachmentFiles.size()) {
|
|
return;
|
|
}
|
|
|
|
const QString removedFile = m_attachmentFiles.at(index);
|
|
m_attachmentFiles.removeAt(index);
|
|
emit attachmentFilesChanged();
|
|
|
|
LOG_MESSAGE(QString("Removed attachment file: %1").arg(removedFile));
|
|
}
|
|
|
|
void ChatRootView::showLinkFilesDialog()
|
|
{
|
|
QFileDialog dialog(nullptr, tr("Select Files to Attach"));
|
|
dialog.setFileMode(QFileDialog::ExistingFiles);
|
|
|
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
|
dialog.setDirectory(project->projectDirectory().toFSPathString());
|
|
}
|
|
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
addFilesToLinkList(dialog.selectedFiles());
|
|
}
|
|
}
|
|
|
|
void ChatRootView::addFilesToLinkList(const QStringList &filePaths)
|
|
{
|
|
if (filePaths.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
bool filesAdded = false;
|
|
QStringList imageFiles;
|
|
|
|
for (const QString &filePath : filePaths) {
|
|
if (isImageFile(filePath)) {
|
|
imageFiles.append(filePath);
|
|
continue;
|
|
}
|
|
|
|
if (!m_linkedFiles.contains(filePath)) {
|
|
m_linkedFiles.append(filePath);
|
|
filesAdded = true;
|
|
}
|
|
}
|
|
|
|
if (!imageFiles.isEmpty()) {
|
|
addFilesToAttachList(imageFiles);
|
|
m_lastInfoMessage
|
|
= tr("Images automatically moved to Attach zone (%n file(s))", "", imageFiles.size());
|
|
emit lastInfoMessageChanged();
|
|
}
|
|
|
|
if (filesAdded) {
|
|
emit linkedFilesChanged();
|
|
}
|
|
}
|
|
|
|
void ChatRootView::removeFileFromLinkList(int index)
|
|
{
|
|
if (index < 0 || index >= m_linkedFiles.size()) {
|
|
return;
|
|
}
|
|
|
|
const QString removedFile = m_linkedFiles.at(index);
|
|
m_linkedFiles.removeAt(index);
|
|
emit linkedFilesChanged();
|
|
|
|
LOG_MESSAGE(QString("Removed linked file: %1").arg(removedFile));
|
|
}
|
|
|
|
void ChatRootView::showAddImageDialog()
|
|
{
|
|
QFileDialog dialog(nullptr, tr("Select Images to Attach"));
|
|
dialog.setFileMode(QFileDialog::ExistingFiles);
|
|
dialog.setNameFilter(tr("Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)"));
|
|
|
|
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
|
|
dialog.setDirectory(project->projectDirectory().toFSPathString());
|
|
}
|
|
|
|
if (dialog.exec() == QDialog::Accepted) {
|
|
addFilesToAttachList(dialog.selectedFiles());
|
|
}
|
|
}
|
|
|
|
QStringList ChatRootView::convertUrlsToLocalPaths(const QVariantList &urls) const
|
|
{
|
|
QStringList localPaths;
|
|
for (const QVariant &urlVariant : urls) {
|
|
QUrl url(urlVariant.toString());
|
|
if (url.isLocalFile()) {
|
|
QString localPath = url.toLocalFile();
|
|
if (!localPath.isEmpty()) {
|
|
localPaths.append(localPath);
|
|
}
|
|
}
|
|
}
|
|
return localPaths;
|
|
}
|
|
|
|
void ChatRootView::calculateMessageTokensCount(const QString &message)
|
|
{
|
|
m_tokenCounter->setMessage(message);
|
|
}
|
|
|
|
void ChatRootView::setIsSyncOpenFiles(bool state)
|
|
{
|
|
if (m_isSyncOpenFiles != state) {
|
|
m_isSyncOpenFiles = state;
|
|
emit isSyncOpenFilesChanged();
|
|
}
|
|
|
|
if (m_isSyncOpenFiles) {
|
|
for (auto editor : std::as_const(m_currentEditors)) {
|
|
onAppendLinkFileFromEditor(editor);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ChatRootView::openChatHistoryFolder()
|
|
{
|
|
m_historyStore->openHistoryFolder();
|
|
}
|
|
|
|
void ChatRootView::openRulesFolder()
|
|
{
|
|
auto project = ProjectExplorer::ProjectManager::startupProject();
|
|
if (!project) {
|
|
return;
|
|
}
|
|
|
|
QString projectPath = project->projectDirectory().toFSPathString();
|
|
QString rulesPath = QDir(projectPath).filePath(".qodeassist/rules");
|
|
|
|
QDir dir(rulesPath);
|
|
if (!dir.exists()) {
|
|
dir.mkpath(".");
|
|
}
|
|
|
|
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
|
QDesktopServices::openUrl(url);
|
|
}
|
|
|
|
void ChatRootView::openSettings()
|
|
{
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[]() { Settings::showSettings(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID); },
|
|
Qt::QueuedConnection);
|
|
}
|
|
|
|
void ChatRootView::openFileInEditor(const QString &filePath)
|
|
{
|
|
if (filePath.isEmpty())
|
|
return;
|
|
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
|
|
}
|
|
|
|
void ChatRootView::updateInputTokensCount()
|
|
{
|
|
m_tokenCounter->recompute();
|
|
}
|
|
|
|
int ChatRootView::inputTokensCount() const
|
|
{
|
|
return m_tokenCounter->inputTokens();
|
|
}
|
|
|
|
bool ChatRootView::isSyncOpenFiles() const
|
|
{
|
|
return m_isSyncOpenFiles;
|
|
}
|
|
|
|
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
|
{
|
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
|
QString filePath = document->filePath().toFSPathString();
|
|
m_linkedFiles.removeOne(filePath);
|
|
emit linkedFilesChanged();
|
|
}
|
|
|
|
if (editor) {
|
|
m_currentEditors.removeOne(editor);
|
|
}
|
|
|
|
emit openFilesChanged();
|
|
}
|
|
|
|
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
|
{
|
|
if (auto document = editor->document(); document && isSyncOpenFiles()) {
|
|
QString filePath = document->filePath().toFSPathString();
|
|
if (!m_linkedFiles.contains(filePath) && !shouldIgnoreFileForAttach(document->filePath())) {
|
|
m_linkedFiles.append(filePath);
|
|
emit linkedFilesChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath)
|
|
{
|
|
if (editor && editor->document()) {
|
|
m_currentEditors.append(editor);
|
|
emit openFilesChanged();
|
|
}
|
|
}
|
|
|
|
QString ChatRootView::chatFileName() const
|
|
{
|
|
return QFileInfo(m_recentFilePath).baseName();
|
|
}
|
|
|
|
QString ChatRootView::chatFilePath() const
|
|
{
|
|
return m_recentFilePath;
|
|
}
|
|
|
|
void ChatRootView::setRecentFilePath(const QString &filePath)
|
|
{
|
|
if (m_recentFilePath != filePath) {
|
|
m_recentFilePath = filePath;
|
|
m_clientInterface->setChatFilePath(filePath);
|
|
m_fileManager->setChatFilePath(filePath);
|
|
emit chatFileNameChanged();
|
|
}
|
|
}
|
|
|
|
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
|
|
{
|
|
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
|
|
if (project
|
|
&& m_clientInterface->contextManager()
|
|
->ignoreManager()
|
|
->shouldIgnore(filePath.toFSPathString(), project)) {
|
|
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
|
|
.arg(filePath.toFSPathString()));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QString ChatRootView::textFontFamily() const
|
|
{
|
|
return Settings::chatAssistantSettings().textFontFamily.stringValue();
|
|
}
|
|
|
|
QString ChatRootView::codeFontFamily() const
|
|
{
|
|
return Settings::chatAssistantSettings().codeFontFamily.stringValue();
|
|
}
|
|
|
|
int ChatRootView::codeFontSize() const
|
|
{
|
|
return Settings::chatAssistantSettings().codeFontSize();
|
|
}
|
|
|
|
int ChatRootView::textFontSize() const
|
|
{
|
|
return Settings::chatAssistantSettings().textFontSize();
|
|
}
|
|
|
|
int ChatRootView::textFormat() const
|
|
{
|
|
return Settings::chatAssistantSettings().textFormat();
|
|
}
|
|
|
|
bool ChatRootView::isRequestInProgress() const
|
|
{
|
|
return m_isRequestInProgress;
|
|
}
|
|
|
|
void ChatRootView::setRequestProgressStatus(bool state)
|
|
{
|
|
if (m_isRequestInProgress == state)
|
|
return;
|
|
m_isRequestInProgress = state;
|
|
emit isRequestInProgressChanged();
|
|
}
|
|
|
|
QString ChatRootView::lastErrorMessage() const
|
|
{
|
|
return m_lastErrorMessage;
|
|
}
|
|
|
|
QVariantList ChatRootView::activeRules() const
|
|
{
|
|
return m_activeRules;
|
|
}
|
|
|
|
int ChatRootView::activeRulesCount() const
|
|
{
|
|
return m_activeRules.size();
|
|
}
|
|
|
|
QString ChatRootView::getRuleContent(int index)
|
|
{
|
|
if (index < 0 || index >= m_activeRules.size())
|
|
return QString();
|
|
|
|
return PluginLLMCore::RulesLoader::loadRuleFileContent(
|
|
m_activeRules[index].toMap()["filePath"].toString());
|
|
}
|
|
|
|
void ChatRootView::refreshRules()
|
|
{
|
|
m_activeRules.clear();
|
|
|
|
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
|
if (!project) {
|
|
emit activeRulesChanged();
|
|
emit activeRulesCountChanged();
|
|
return;
|
|
}
|
|
|
|
auto ruleFiles
|
|
= PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat);
|
|
|
|
for (const auto &ruleFile : ruleFiles) {
|
|
QVariantMap ruleMap;
|
|
ruleMap["filePath"] = ruleFile.filePath;
|
|
ruleMap["fileName"] = ruleFile.fileName;
|
|
ruleMap["category"] = ruleFile.category;
|
|
m_activeRules.append(ruleMap);
|
|
}
|
|
|
|
emit activeRulesChanged();
|
|
emit activeRulesCountChanged();
|
|
}
|
|
|
|
bool ChatRootView::useTools() const
|
|
{
|
|
return Settings::chatAssistantSettings().enableChatTools();
|
|
}
|
|
|
|
void ChatRootView::setUseTools(bool enabled)
|
|
{
|
|
Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
|
|
Settings::chatAssistantSettings().writeSettings();
|
|
}
|
|
|
|
bool ChatRootView::useThinking() const
|
|
{
|
|
return Settings::chatAssistantSettings().enableThinkingMode();
|
|
}
|
|
|
|
void ChatRootView::setUseThinking(bool enabled)
|
|
{
|
|
Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
|
|
Settings::chatAssistantSettings().writeSettings();
|
|
}
|
|
|
|
void ChatRootView::applyFileEdit(const QString &editId)
|
|
{
|
|
m_fileEditController->applyFileEdit(editId);
|
|
}
|
|
|
|
void ChatRootView::rejectFileEdit(const QString &editId)
|
|
{
|
|
m_fileEditController->rejectFileEdit(editId);
|
|
}
|
|
|
|
void ChatRootView::undoFileEdit(const QString &editId)
|
|
{
|
|
m_fileEditController->undoFileEdit(editId);
|
|
}
|
|
|
|
void ChatRootView::openFileEditInEditor(const QString &editId)
|
|
{
|
|
m_fileEditController->openFileEditInEditor(editId);
|
|
}
|
|
|
|
void ChatRootView::applyAllFileEditsForCurrentMessage()
|
|
{
|
|
m_fileEditController->applyAllForCurrentMessage();
|
|
}
|
|
|
|
void ChatRootView::undoAllFileEditsForCurrentMessage()
|
|
{
|
|
m_fileEditController->undoAllForCurrentMessage();
|
|
}
|
|
|
|
void ChatRootView::updateCurrentMessageEditsStats()
|
|
{
|
|
m_fileEditController->updateStats();
|
|
}
|
|
|
|
int ChatRootView::currentMessageTotalEdits() const
|
|
{
|
|
return m_fileEditController->totalEdits();
|
|
}
|
|
|
|
int ChatRootView::currentMessageAppliedEdits() const
|
|
{
|
|
return m_fileEditController->appliedEdits();
|
|
}
|
|
|
|
int ChatRootView::currentMessagePendingEdits() const
|
|
{
|
|
return m_fileEditController->pendingEdits();
|
|
}
|
|
|
|
int ChatRootView::currentMessageRejectedEdits() const
|
|
{
|
|
return m_fileEditController->rejectedEdits();
|
|
}
|
|
|
|
QString ChatRootView::lastInfoMessage() const
|
|
{
|
|
return m_lastInfoMessage;
|
|
}
|
|
|
|
bool ChatRootView::isThinkingSupport() const
|
|
{
|
|
auto providerName = Settings::generalSettings().caProvider();
|
|
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
|
|
|
return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking);
|
|
}
|
|
|
|
bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
|
|
{
|
|
for (const QString &filePath : attachments) {
|
|
if (isImageFile(filePath)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ChatRootView::isImageFile(const QString &filePath) const
|
|
{
|
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
|
|
|
QFileInfo fileInfo(filePath);
|
|
return imageExtensions.contains(fileInfo.suffix().toLower());
|
|
}
|
|
|
|
void ChatRootView::loadAvailableConfigurations()
|
|
{
|
|
m_configurationController->loadAvailableConfigurations();
|
|
}
|
|
|
|
void ChatRootView::applyConfiguration(const QString &configName)
|
|
{
|
|
m_configurationController->applyConfiguration(configName);
|
|
}
|
|
|
|
QStringList ChatRootView::availableConfigurations() const
|
|
{
|
|
return m_configurationController->availableConfigurations();
|
|
}
|
|
|
|
QString ChatRootView::currentConfiguration() const
|
|
{
|
|
return m_configurationController->currentConfiguration();
|
|
}
|
|
|
|
void ChatRootView::loadAvailableAgentRoles()
|
|
{
|
|
m_agentRoleController->loadAvailableRoles();
|
|
}
|
|
|
|
void ChatRootView::applyAgentRole(const QString &roleName)
|
|
{
|
|
m_agentRoleController->applyRole(roleName);
|
|
}
|
|
|
|
QStringList ChatRootView::availableAgentRoles() const
|
|
{
|
|
return m_agentRoleController->availableRoles();
|
|
}
|
|
|
|
QString ChatRootView::currentAgentRole() const
|
|
{
|
|
return m_agentRoleController->currentRole();
|
|
}
|
|
|
|
QString ChatRootView::baseSystemPrompt() const
|
|
{
|
|
return m_agentRoleController->baseSystemPrompt();
|
|
}
|
|
|
|
QString ChatRootView::currentAgentRoleDescription() const
|
|
{
|
|
return m_agentRoleController->currentRoleDescription();
|
|
}
|
|
|
|
QString ChatRootView::currentAgentRoleSystemPrompt() const
|
|
{
|
|
return m_agentRoleController->currentRoleSystemPrompt();
|
|
}
|
|
|
|
void ChatRootView::openAgentRolesSettings()
|
|
{
|
|
m_agentRoleController->openSettings();
|
|
}
|
|
|
|
void ChatRootView::compressCurrentChat()
|
|
{
|
|
if (m_chatCompressor->isCompressing()) {
|
|
m_lastErrorMessage = tr("Compression is already in progress");
|
|
emit lastErrorMessageChanged();
|
|
return;
|
|
}
|
|
|
|
if (m_recentFilePath.isEmpty()) {
|
|
m_lastErrorMessage = tr("No chat file to compress. Please save the chat first.");
|
|
emit lastErrorMessageChanged();
|
|
return;
|
|
}
|
|
|
|
autosave();
|
|
|
|
m_chatCompressor->startCompression(m_recentFilePath, m_chatModel);
|
|
}
|
|
|
|
void ChatRootView::cancelCompression()
|
|
{
|
|
m_chatCompressor->cancelCompression();
|
|
}
|
|
|
|
bool ChatRootView::isCompressing() const
|
|
{
|
|
return m_chatCompressor->isCompressing();
|
|
}
|
|
|
|
} // namespace QodeAssist::Chat
|