Files
QodeAssist/ChatView/ChatRootView.cpp
2026-05-19 09:46:50 +02:00

1250 lines
34 KiB
C++

// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatRootView.hpp"
#include <QAction>
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QQmlContext>
#include <QQmlEngine>
#include <QTextStream>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#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 "QodeAssistConstants.hpp"
#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 "SessionFileRegistry.hpp"
#include "context/ContextManager.hpp"
#include "pluginllmcore/RulesLoader.hpp"
#include "ProjectSettings.hpp"
#include "SkillsSettings.hpp"
#include "sources/skills/SkillsManager.hpp"
namespace QodeAssist::Chat {
namespace {
bool isChatEditor(Core::IEditor *editor)
{
return editor && editor->document()
&& editor->document()->id() == Utils::Id(Constants::QODE_ASSIST_CHAT_EDITOR_ID);
}
} // namespace
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);
}
});
}
ChatRootView::~ChatRootView()
{
if (m_sessionFileRegistry && !m_recentFilePath.isEmpty()) {
m_sessionFileRegistry->release(m_recentFilePath);
}
}
SessionFileRegistry *ChatRootView::sessionFileRegistry() const
{
if (!m_sessionFileRegistryResolved) {
m_sessionFileRegistryResolved = true;
if (auto context = qmlContext(this)) {
m_sessionFileRegistry = qobject_cast<SessionFileRegistry *>(
context->contextProperty("sessionFileRegistry").value<QObject *>());
}
}
return m_sessionFileRegistry;
}
Skills::SkillsManager *ChatRootView::skillsManager() const
{
if (!m_skillsManagerResolved) {
m_skillsManagerResolved = true;
if (auto context = qmlContext(this)) {
m_skillsManager = qobject_cast<Skills::SkillsManager *>(
context->contextProperty("skillsManager").value<QObject *>());
}
}
return m_skillsManager;
}
QVariantList ChatRootView::searchSkills(const QString &query) const
{
QVariantList results;
auto *manager = skillsManager();
if (!manager || !Settings::skillsSettings().enableSkills())
return results;
auto *project = PluginLLMCore::RulesLoader::getActiveProject();
QStringList projectSkillDirs;
if (project) {
Settings::ProjectSettings projectSettings(project);
projectSkillDirs = Settings::SkillsSettings::splitLines(
projectSettings.projectSkillDirs());
}
manager->configure(
project ? project->projectDirectory().toFSPathString() : QString(),
Settings::SkillsSettings::splitPaths(Settings::skillsSettings().globalSkillRoots()),
projectSkillDirs);
const QString needle = query.trimmed().toLower();
for (const Skills::AgentSkill &skill : manager->skills()) {
if (!skill.enabled)
continue;
if (!needle.isEmpty() && !skill.name.toLower().contains(needle)
&& !skill.description.toLower().contains(needle)) {
continue;
}
results.append(QVariantMap{
{QStringLiteral("name"), skill.name},
{QStringLiteral("description"), skill.description}});
}
return results;
}
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 (auto registry = sessionFileRegistry()) {
filePath = registry->uniqueFreePath(filePath);
}
if (!filePath.isEmpty()) {
setRecentFilePath(filePath);
LOG_MESSAGE(QString("Set chat file path for new chat: %1").arg(filePath));
}
}
m_tokenCounter->recordSent();
m_clientInterface->setSkillsManager(skillsManager());
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)
{
if (filePath != m_recentFilePath) {
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
m_lastErrorMessage
= tr("This chat file is already in use by another QodeAssist chat session.");
emit lastErrorMessageChanged();
return;
}
}
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)
{
if (filePath != m_recentFilePath) {
if (auto registry = sessionFileRegistry(); registry && registry->isLocked(filePath)) {
m_lastErrorMessage
= tr("This chat is already open in another QodeAssist chat session.");
emit lastErrorMessageChanged();
return;
}
}
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;
}
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath();
if (auto registry = sessionFileRegistry()) {
filePath = registry->uniqueFreePath(filePath);
}
if (filePath.isEmpty()) {
return;
}
setRecentFilePath(filePath);
}
m_historyStore->save(m_recentFilePath);
}
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::triggerOpenChatCommand(Utils::Id commandId)
{
if (auto command = Core::ActionManager::command(commandId)) {
if (auto action = command->action())
action->trigger();
}
}
void ChatRootView::handOffSession()
{
if (m_chatModel->rowCount() > 0) {
if (m_recentFilePath.isEmpty()) {
QString filePath = getAutosaveFilePath();
if (auto registry = sessionFileRegistry())
filePath = registry->uniqueFreePath(filePath);
if (!filePath.isEmpty())
setRecentFilePath(filePath);
}
if (!m_recentFilePath.isEmpty())
m_historyStore->save(m_recentFilePath);
}
if (auto registry = sessionFileRegistry(); registry && !m_recentFilePath.isEmpty())
registry->setPendingChatFile(m_recentFilePath);
setRecentFilePath(QString{});
}
void ChatRootView::consumePendingChatFile()
{
if (auto registry = sessionFileRegistry()) {
const QString pending = registry->takePendingChatFile();
if (!pending.isEmpty())
loadHistory(pending);
}
}
void ChatRootView::relocateToSplit()
{
handOffSession();
triggerOpenChatCommand(Constants::QODE_ASSIST_SHOW_CHAT_ACTION);
clearMessages();
clearAttachmentFiles();
emit closeHostRequested();
}
void ChatRootView::relocateToWindow()
{
handOffSession();
triggerOpenChatCommand(Constants::QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION);
clearMessages();
clearAttachmentFiles();
emit closeHostRequested();
// Closing the source split raises the main window; re-raise the chat window once that
// queued teardown has run. The registry outlives this view, which the split close deletes.
if (auto registry = sessionFileRegistry()) {
QMetaObject::invokeMethod(
registry,
[] {
if (auto command = Core::ActionManager::command(
Constants::QODE_ASSIST_OPEN_CHAT_WINDOW_ACTION)) {
if (auto action = command->action())
action->trigger();
}
},
Qt::QueuedConnection);
}
}
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 (isChatEditor(editor)) {
return;
}
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 (isChatEditor(editor)) {
return;
}
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 (isChatEditor(editor)) {
return;
}
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) {
return;
}
if (auto registry = sessionFileRegistry()) {
if (!m_recentFilePath.isEmpty()) {
registry->release(m_recentFilePath);
}
if (!filePath.isEmpty()) {
registry->lock(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