From 85a7bba90eaf5cba0c22e2bfab8f517365b9cb4d Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Thu, 27 Nov 2025 00:39:37 +0100 Subject: [PATCH] refactor: Change top bar layout and tools/thinking settings --- ChatView/CMakeLists.txt | 2 + ChatView/ChatRootView.cpp | 247 +++++++++++++-------------- ChatView/ChatRootView.hpp | 22 +-- ChatView/ClientInterface.cpp | 107 ++++++------ ChatView/ClientInterface.hpp | 3 +- ChatView/icons/thinking-icon-off.svg | 4 +- ChatView/icons/tools-icon-off.svg | 11 ++ ChatView/icons/tools-icon-on.svg | 10 ++ ChatView/qml/RootItem.qml | 13 +- ChatView/qml/controls/TopBar.qml | 56 +++--- UIControls/qml/QoAComboBox.qml | 4 + settings/ChatAssistantSettings.cpp | 23 ++- settings/ChatAssistantSettings.hpp | 1 + settings/SettingsConstants.hpp | 1 + settings/ToolsSettings.cpp | 10 -- settings/ToolsSettings.hpp | 1 - 16 files changed, 274 insertions(+), 241 deletions(-) create mode 100644 ChatView/icons/tools-icon-off.svg create mode 100644 ChatView/icons/tools-icon-on.svg diff --git a/ChatView/CMakeLists.txt b/ChatView/CMakeLists.txt index 4f8e89a..4cb0c29 100644 --- a/ChatView/CMakeLists.txt +++ b/ChatView/CMakeLists.txt @@ -49,6 +49,8 @@ qt_add_qml_module(QodeAssistChatView icons/reject-changes-button.svg icons/thinking-icon-on.svg icons/thinking-icon-off.svg + icons/tools-icon-on.svg + icons/tools-icon-off.svg SOURCES ChatWidget.hpp ChatWidget.cpp diff --git a/ChatView/ChatRootView.cpp b/ChatView/ChatRootView.cpp index ef92b31..22fa1a8 100644 --- a/ChatView/ChatRootView.cpp +++ b/ChatView/ChatRootView.cpp @@ -37,14 +37,14 @@ #include "ChatSerializer.hpp" #include "ConfigurationManager.hpp" #include "GeneralSettings.hpp" -#include "ToolsSettings.hpp" #include "Logger.hpp" #include "ProjectSettings.hpp" +#include "ProvidersManager.hpp" +#include "ToolsSettings.hpp" #include "context/ChangesManager.h" #include "context/ContextManager.hpp" #include "context/TokenUtils.hpp" #include "llmcore/RulesLoader.hpp" -#include "ProvidersManager.hpp" namespace QodeAssist::Chat { @@ -66,18 +66,18 @@ ChatRootView::ChatRootView(QQuickItem *parent) connect( &settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged); - + connect(&settings.caProvider, &Utils::BaseAspect::changed, this, [this]() { auto &settings = Settings::generalSettings(); - m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(), - settings.caModel.value()); + m_currentConfiguration + = QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value()); emit currentConfigurationChanged(); }); - + connect(&settings.caModel, &Utils::BaseAspect::changed, this, [this]() { auto &settings = Settings::generalSettings(); - m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(), - settings.caModel.value()); + m_currentConfiguration + = QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value()); emit currentConfigurationChanged(); }); @@ -97,7 +97,7 @@ ChatRootView::ChatRootView(QQuickItem *parent) this, &ChatRootView::updateInputTokensCount); - connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { + connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); m_currentMessageRequestId.clear(); updateCurrentMessageEditsStats(); @@ -161,41 +161,42 @@ ChatRootView::ChatRootView(QQuickItem *parent) m_lastErrorMessage = error; emit lastErrorMessageChanged(); }); - + connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) { if (!m_currentMessageRequestId.isEmpty()) { - LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId)); + LOG_MESSAGE( + QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId)); } - + m_currentMessageRequestId = requestId; LOG_MESSAGE(QString("New message request started: %1").arg(requestId)); updateCurrentMessageEditsStats(); }); - + connect( &Context::ChangesManager::instance(), &Context::ChangesManager::fileEditAdded, this, [this](const QString &) { updateCurrentMessageEditsStats(); }); - + connect( &Context::ChangesManager::instance(), &Context::ChangesManager::fileEditApplied, this, [this](const QString &) { updateCurrentMessageEditsStats(); }); - + connect( &Context::ChangesManager::instance(), &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, @@ -212,21 +213,18 @@ ChatRootView::ChatRootView(QQuickItem *parent) this, &ChatRootView::refreshRules); - QSettings appSettings; - m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool(); - m_isThinkingMode = Settings::chatAssistantSettings().enableThinkingMode(); + connect( + &Settings::chatAssistantSettings().enableChatTools, + &Utils::BaseAspect::changed, + this, + &ChatRootView::useToolsChanged); connect( &Settings::chatAssistantSettings().enableThinkingMode, &Utils::BaseAspect::changed, this, - [this]() { setIsThinkingMode(Settings::chatAssistantSettings().enableThinkingMode()); }); + &ChatRootView::useThinkingChanged); - connect( - &Settings::toolsSettings().useTools, - &Utils::BaseAspect::changed, - this, - &ChatRootView::toolsSupportEnabledChanged); connect( &Settings::generalSettings().caProvider, &Utils::BaseAspect::changed, @@ -265,7 +263,8 @@ void ChatRootView::sendMessage(const QString &message) } } - m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles, m_isAgentMode); + m_clientInterface + ->sendMessage(message, m_attachmentFiles, m_linkedFiles, useTools(), useThinking()); clearAttachmentFiles(); setRequestProgressStatus(true); } @@ -342,7 +341,7 @@ void ChatRootView::loadHistory(const QString &filePath) } else { setRecentFilePath(filePath); } - + m_currentMessageRequestId.clear(); updateInputTokensCount(); updateCurrentMessageEditsStats(); @@ -410,7 +409,8 @@ QString ChatRootView::getSuggestedFileName() const shortMessage = firstMessage.split('\n').first().simplified().left(30); if (shortMessage.isEmpty()) { - QVariantList images = m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList(); + QVariantList images + = m_chatModel->data(m_chatModel->index(0), ChatModel::Images).toList(); if (!images.isEmpty()) { shortMessage = "image_chat"; } @@ -447,7 +447,8 @@ QString ChatRootView::getAutosaveFilePath() const return QDir(dir).filePath(getSuggestedFileName() + ".json"); } -QString ChatRootView::getAutosaveFilePath(const QString &firstMessage, const QStringList &attachments) const +QString ChatRootView::getAutosaveFilePath( + const QString &firstMessage, const QStringList &attachments) const { if (!m_recentFilePath.isEmpty()) { return m_recentFilePath; @@ -505,7 +506,7 @@ void ChatRootView::addFilesToAttachList(const QStringList &filePaths) filesAdded = true; } } - + if (filesAdded) { emit attachmentFilesChanged(); } @@ -541,26 +542,27 @@ void ChatRootView::addFilesToLinkList(const QStringList &filePaths) 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()); + + m_lastInfoMessage + = tr("Images automatically moved to Attach zone (%n file(s))", "", imageFiles.size()); emit lastInfoMessageChanged(); } - + if (filesAdded) { emit linkedFilesChanged(); } @@ -867,43 +869,26 @@ void ChatRootView::refreshRules() emit activeRulesCountChanged(); } -bool ChatRootView::isAgentMode() const +bool ChatRootView::useTools() const { - return m_isAgentMode; + return Settings::chatAssistantSettings().enableChatTools(); } -void ChatRootView::setIsAgentMode(bool newIsAgentMode) +void ChatRootView::setUseTools(bool enabled) { - if (m_isAgentMode != newIsAgentMode) { - m_isAgentMode = newIsAgentMode; - - QSettings settings; - settings.setValue("QodeAssist/Chat/AgentMode", newIsAgentMode); - - emit isAgentModeChanged(); - } + Settings::chatAssistantSettings().enableChatTools.setValue(enabled); + Settings::chatAssistantSettings().writeSettings(); } -bool ChatRootView::isThinkingMode() const +bool ChatRootView::useThinking() const { - return m_isThinkingMode; + return Settings::chatAssistantSettings().enableThinkingMode(); } -void ChatRootView::setIsThinkingMode(bool newIsThinkingMode) +void ChatRootView::setUseThinking(bool enabled) { - if (m_isThinkingMode != newIsThinkingMode) { - m_isThinkingMode = newIsThinkingMode; - - Settings::chatAssistantSettings().enableThinkingMode.setValue(newIsThinkingMode); - Settings::chatAssistantSettings().writeSettings(); - - emit isThinkingModeChanged(); - } -} - -bool ChatRootView::toolsSupportEnabled() const -{ - return Settings::toolsSettings().useTools(); + Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled); + Settings::chatAssistantSettings().writeSettings(); } void ChatRootView::applyFileEdit(const QString &editId) @@ -912,13 +897,13 @@ void ChatRootView::applyFileEdit(const QString &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); + m_lastErrorMessage = edit.statusMessage.isEmpty() + ? QString("Failed to apply file edit") + : QString("Failed to apply file edit: %1").arg(edit.statusMessage); emit lastErrorMessageChanged(); } } @@ -929,13 +914,13 @@ void ChatRootView::rejectFileEdit(const QString &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); + m_lastErrorMessage = edit.statusMessage.isEmpty() + ? QString("Failed to reject file edit") + : QString("Failed to reject file edit: %1").arg(edit.statusMessage); emit lastErrorMessageChanged(); } } @@ -946,13 +931,13 @@ void ChatRootView::undoFileEdit(const QString &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); + m_lastErrorMessage = edit.statusMessage.isEmpty() + ? QString("Failed to undo file edit") + : QString("Failed to undo file edit: %1").arg(edit.statusMessage); emit lastErrorMessageChanged(); } } @@ -960,37 +945,36 @@ void ChatRootView::undoFileEdit(const QString &editId) void ChatRootView::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()) { 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(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()) { + } else if (!edit.oldContent.isEmpty()) { position = currentContent.indexOf(edit.oldContent); } - + if (position >= 0) { QTextCursor cursor(doc); cursor.setPosition(position); @@ -999,7 +983,7 @@ void ChatRootView::openFileEditInEditor(const QString &editId) } } } - + LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath)); } @@ -1009,33 +993,35 @@ void ChatRootView::updateFileEditStatus(const QString &editId, const QString &st 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)); + + 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(); } @@ -1046,37 +1032,39 @@ void ChatRootView::applyAllFileEditsForCurrentMessage() 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); + + 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); + 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); + + auto edits = Context::ChangesManager::instance().getEditsForRequest( + m_currentMessageRequestId); for (const auto &edit : edits) { if (edit.status == Context::ChangesManager::Applied) { updateFileEditStatus(edit.editId, "applied"); } } } - + updateCurrentMessageEditsStats(); } @@ -1087,45 +1075,47 @@ void ChatRootView::undoAllFileEditsForCurrentMessage() 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); + + 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); + 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); + + 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() { if (m_currentMessageRequestId.isEmpty()) { - if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 || - m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) { + if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 + || m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) { m_currentMessageTotalEdits = 0; m_currentMessageAppliedEdits = 0; m_currentMessagePendingEdits = 0; @@ -1134,14 +1124,14 @@ void ChatRootView::updateCurrentMessageEditsStats() } 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: @@ -1158,7 +1148,7 @@ void ChatRootView::updateCurrentMessageEditsStats() break; } } - + bool changed = false; if (m_currentMessageTotalEdits != total) { m_currentMessageTotalEdits = total; @@ -1176,10 +1166,14 @@ void ChatRootView::updateCurrentMessageEditsStats() 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)); + 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(); } } @@ -1268,9 +1262,7 @@ bool ChatRootView::hasImageAttachments(const QStringList &attachments) const bool ChatRootView::isImageFile(const QString &filePath) const { - static const QSet imageExtensions = { - "png", "jpg", "jpeg", "gif", "webp", "bmp", "svg" - }; + static const QSet imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"}; QFileInfo fileInfo(filePath); return imageExtensions.contains(fileInfo.suffix().toLower()); @@ -1318,7 +1310,8 @@ void ChatRootView::applyConfiguration(const QString &configName) settings.caModel.setValue(config.model); settings.caTemplate.setValue(config.templateName); settings.caUrl.setValue(config.url); - settings.caEndpointMode.setValue(settings.caEndpointMode.indexForDisplay(config.endpointMode)); + settings.caEndpointMode.setValue( + settings.caEndpointMode.indexForDisplay(config.endpointMode)); settings.caCustomEndpoint.setValue(config.customEndpoint); settings.writeSettings(); diff --git a/ChatView/ChatRootView.hpp b/ChatView/ChatRootView.hpp index 55480d5..5e011e5 100644 --- a/ChatView/ChatRootView.hpp +++ b/ChatView/ChatRootView.hpp @@ -48,10 +48,8 @@ class ChatRootView : public QQuickItem Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL) Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL) - Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL) - Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL) - Q_PROPERTY( - bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL) + Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL) + Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL) Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL) @@ -127,11 +125,10 @@ public: Q_INVOKABLE QString getRuleContent(int index); Q_INVOKABLE void refreshRules(); - bool isAgentMode() const; - void setIsAgentMode(bool newIsAgentMode); - bool isThinkingMode() const; - void setIsThinkingMode(bool newIsThinkingMode); - bool toolsSupportEnabled() const; + bool useTools() const; + void setUseTools(bool enabled); + bool useThinking() const; + void setUseThinking(bool enabled); Q_INVOKABLE void applyFileEdit(const QString &editId); Q_INVOKABLE void rejectFileEdit(const QString &editId); @@ -184,9 +181,8 @@ signals: void activeRulesChanged(); void activeRulesCountChanged(); - void isAgentModeChanged(); - void isThinkingModeChanged(); - void toolsSupportEnabledChanged(); + void useToolsChanged(); + void useThinkingChanged(); void currentMessageEditsStatsChanged(); void isThinkingSupportChanged(); @@ -214,8 +210,6 @@ private: bool m_isRequestInProgress; QString m_lastErrorMessage; QVariantList m_activeRules; - bool m_isAgentMode; - bool m_isThinkingMode; QString m_currentMessageRequestId; int m_currentMessageTotalEdits{0}; diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 0bbb0f4..f8c805c 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -43,12 +43,12 @@ #include "ChatAssistantSettings.hpp" #include "ChatSerializer.hpp" #include "GeneralSettings.hpp" -#include "ToolsSettings.hpp" #include "Logger.hpp" #include "ProvidersManager.hpp" #include "RequestConfig.hpp" -#include +#include "ToolsSettings.hpp" #include +#include namespace QodeAssist::Chat { @@ -69,16 +69,17 @@ void ClientInterface::sendMessage( const QString &message, const QList &attachments, const QList &linkedFiles, - bool useAgentMode) + bool useTools, + bool useThinking) { cancelRequest(); m_accumulatedResponses.clear(); - + Context::ChangesManager::instance().archiveAllNonArchivedEdits(); QList imageFiles; QList textFiles; - + for (const QString &filePath : attachments) { if (isImageFile(filePath)) { imageFiles.append(filePath); @@ -88,7 +89,7 @@ void ClientInterface::sendMessage( } auto attachFiles = m_contextManager->getContentFiles(textFiles); - + QList imageAttachments; if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { for (const QString &imagePath : imageFiles) { @@ -96,23 +97,25 @@ void ClientInterface::sendMessage( if (base64Data.isEmpty()) { continue; } - + QString storedPath; QFileInfo fileInfo(imagePath); - if (ChatSerializer::saveImageToStorage(m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) { + if (ChatSerializer::saveImageToStorage( + m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) { ChatModel::ImageAttachment imageAttachment; imageAttachment.fileName = fileInfo.fileName(); imageAttachment.storedPath = storedPath; imageAttachment.mediaType = getMediaTypeForImage(imagePath); imageAttachments.append(imageAttachment); - + LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath)); } } } else if (!imageFiles.isEmpty()) { - LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)").arg(imageFiles.size())); + LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)") + .arg(imageFiles.size())); } - + m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles, imageAttachments); auto &chatAssistantSettings = Settings::chatAssistantSettings(); @@ -135,7 +138,7 @@ void ClientInterface::sendMessage( LLMCore::ContextData context; - const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode; + const bool isToolsEnabled = useTools; if (chatAssistantSettings.useSystemPrompt()) { QString systemPrompt = chatAssistantSettings.systemPrompt(); @@ -144,8 +147,9 @@ void ClientInterface::sendMessage( if (project) { systemPrompt += QString("\n# Active project name: %1").arg(project->displayName()); - systemPrompt += QString("\n# Active Project path: %1").arg(project->projectDirectory().toUrlishString()); - + systemPrompt += QString("\n# Active Project path: %1") + .arg(project->projectDirectory().toUrlishString()); + if (auto target = project->activeTarget()) { if (auto buildConfig = target->activeBuildConfiguration()) { systemPrompt += QString("\n# Active Build directory: %1") @@ -174,29 +178,29 @@ void ClientInterface::sendMessage( if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) { continue; } - + LLMCore::Message apiMessage; apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant"; apiMessage.content = msg.content; apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking); apiMessage.isRedacted = msg.isRedacted; apiMessage.signature = msg.signature; - + if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) { auto apiImages = loadImagesFromStorage(msg.images); if (!apiImages.isEmpty()) { apiMessage.images = apiImages; } } - + messages.append(apiMessage); } - + if (!imageFiles.isEmpty() && !provider->supportImage()) { LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored") .arg(provider->name(), QString::number(imageFiles.size()))); } - + context.history = messages; LLMCore::LLMConfig config; @@ -224,14 +228,14 @@ void ClientInterface::sendMessage( promptTemplate, context, LLMCore::RequestType::Chat, - isToolsEnabled, - Settings::chatAssistantSettings().enableThinkingMode()); + useTools, + useThinking); QString requestId = QUuid::createUuid().toString(); QJsonObject request{{"id", requestId}}; m_activeRequests[requestId] = {request, provider}; - + emit requestStarted(requestId); connect( @@ -395,14 +399,14 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString const RequestContext &ctx = it.value(); QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId]; - + QString applyError; - bool applySuccess = Context::ChangesManager::instance() - .applyPendingEditsForRequest(requestId, &applyError); - + bool applySuccess + = Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError); + if (!applySuccess) { LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2") - .arg(requestId, applyError)); + .arg(requestId, applyError)); } LOG_MESSAGE( @@ -443,35 +447,32 @@ void ClientInterface::handleCleanAccumulatedData(const QString &requestId) bool ClientInterface::isImageFile(const QString &filePath) const { - static const QSet imageExtensions = { - "png", "jpg", "jpeg", "gif", "webp", "bmp", "svg" - }; - + static const QSet imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"}; + QFileInfo fileInfo(filePath); QString extension = fileInfo.suffix().toLower(); - + return imageExtensions.contains(extension); } QString ClientInterface::getMediaTypeForImage(const QString &filePath) const { - static const QHash mediaTypes = { - {"png", "image/png"}, - {"jpg", "image/jpeg"}, - {"jpeg", "image/jpeg"}, - {"gif", "image/gif"}, - {"webp", "image/webp"}, - {"bmp", "image/bmp"}, - {"svg", "image/svg+xml"} - }; - + static const QHash mediaTypes + = {{"png", "image/png"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"gif", "image/gif"}, + {"webp", "image/webp"}, + {"bmp", "image/bmp"}, + {"svg", "image/svg+xml"}}; + QFileInfo fileInfo(filePath); QString extension = fileInfo.suffix().toLower(); - + if (mediaTypes.contains(extension)) { return mediaTypes[extension]; } - + QMimeDatabase mimeDb; QMimeType mimeType = mimeDb.mimeTypeForFile(filePath); return mimeType.name(); @@ -484,32 +485,34 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const LOG_MESSAGE(QString("Failed to open image file: %1").arg(filePath)); return QString(); } - + QByteArray imageData = file.readAll(); file.close(); - + return imageData.toBase64(); } -QVector ClientInterface::loadImagesFromStorage(const QList &storedImages) const +QVector ClientInterface::loadImagesFromStorage( + const QList &storedImages) const { QVector apiImages; - + for (const auto &storedImage : storedImages) { - QString base64Data = ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath); + QString base64Data + = ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath); if (base64Data.isEmpty()) { LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath)); continue; } - + LLMCore::ImageAttachment apiImage; apiImage.data = base64Data; apiImage.mediaType = storedImage.mediaType; apiImage.isUrl = false; - + apiImages.append(apiImage); } - + return apiImages; } diff --git a/ChatView/ClientInterface.hpp b/ChatView/ClientInterface.hpp index f944d42..0a4f782 100644 --- a/ChatView/ClientInterface.hpp +++ b/ChatView/ClientInterface.hpp @@ -43,7 +43,8 @@ public: const QString &message, const QList &attachments = {}, const QList &linkedFiles = {}, - bool useAgentMode = false); + bool useTools = false, + bool useThinking = false); void clearMessages(); void cancelRequest(); diff --git a/ChatView/icons/thinking-icon-off.svg b/ChatView/icons/thinking-icon-off.svg index f456a72..c20e84e 100644 --- a/ChatView/icons/thinking-icon-off.svg +++ b/ChatView/icons/thinking-icon-off.svg @@ -1,4 +1,4 @@ - - + + diff --git a/ChatView/icons/tools-icon-off.svg b/ChatView/icons/tools-icon-off.svg new file mode 100644 index 0000000..4c4fb25 --- /dev/null +++ b/ChatView/icons/tools-icon-off.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ChatView/icons/tools-icon-on.svg b/ChatView/icons/tools-icon-on.svg new file mode 100644 index 0000000..91a911d --- /dev/null +++ b/ChatView/icons/tools-icon-on.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ChatView/qml/RootItem.qml b/ChatView/qml/RootItem.qml index c61cdc3..5382380 100644 --- a/ChatView/qml/RootItem.qml +++ b/ChatView/qml/RootItem.qml @@ -103,18 +103,17 @@ ChatRootView { checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false onCheckedChanged: _chatview.isPin = topBar.pinButton.checked } - agentModeSwitch { - checked: root.isAgentMode - enabled: root.toolsSupportEnabled - onToggled: { - root.isAgentMode = agentModeSwitch.checked + toolsButton { + checked: root.useTools + onCheckedChanged: { + root.useTools = toolsButton.checked } } thinkingMode { - checked: root.isThinkingMode + checked: root.useThinking enabled: root.isThinkingSupport onCheckedChanged: { - root.isThinkingMode = thinkingMode.checked + root.useThinking = thinkingMode.checked } } configSelector { diff --git a/ChatView/qml/controls/TopBar.qml b/ChatView/qml/controls/TopBar.qml index 71d8151..b83268c 100644 --- a/ChatView/qml/controls/TopBar.qml +++ b/ChatView/qml/controls/TopBar.qml @@ -34,7 +34,7 @@ Rectangle { property alias openChatHistory: openChatHistoryId property alias pinButton: pinButtonId property alias rulesButton: rulesButtonId - property alias agentModeSwitch: agentModeSwitchId + property alias toolsButton: toolsButtonId property alias thinkingMode: thinkingModeId property alias activeRulesCount: activeRulesCountId.text property alias configSelector: configSelectorId @@ -53,7 +53,8 @@ Rectangle { spacing: 10 Row { - height: agentModeSwitchId.height + id: firstRow + spacing: 10 QoAButton { @@ -75,23 +76,44 @@ Rectangle { : qsTr("Pin chat window to the top") } - QoATextSlider { - id: agentModeSwitchId + QoAComboBox { + id: configSelectorId + + implicitHeight: 25 + + model: [] + currentIndex: 0 + + ToolTip.visible: hovered + ToolTip.delay: 250 + ToolTip.text: qsTr("Switch AI configuration") + } + + QoAButton { + id: toolsButtonId anchors.verticalCenter: parent.verticalCenter - leftText: "chat" - rightText: "AI Agent" + checkable: true + opacity: enabled ? 1.0 : 0.2 + + icon { + source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg" + : "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg" + color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF" + height: 15 + width: 15 + } ToolTip.visible: hovered ToolTip.delay: 250 ToolTip.text: { - if (!agentModeSwitchId.enabled) { + if (!toolsButtonId.enabled) { return qsTr("Tools are disabled in General Settings") } return checked - ? qsTr("Agent Mode: AI can use tools to read files, search project, and build code") - : qsTr("Chat Mode: Simple conversation without tool access") + ? qsTr("Tools enabled: AI can use tools to read files, search project, and build code") + : qsTr("Tools disabled: Simple conversation without tool access") } } @@ -120,7 +142,7 @@ Rectangle { } Item { - height: agentModeSwitchId.height + height: firstRow.height width: recentPathId.width Text { @@ -144,7 +166,10 @@ Rectangle { } RowLayout { + id: secondRow + Layout.preferredWidth: root.width + Layout.preferredHeight: firstRow.height spacing: 10 @@ -239,17 +264,6 @@ Rectangle { ToolTip.delay: 250 ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold") } - - QoAComboBox { - id: configSelectorId - - model: [] - currentIndex: 0 - - ToolTip.visible: hovered - ToolTip.delay: 250 - ToolTip.text: qsTr("Switch AI configuration") - } } } } diff --git a/UIControls/qml/QoAComboBox.qml b/UIControls/qml/QoAComboBox.qml index 48964ba..eca178c 100644 --- a/UIControls/qml/QoAComboBox.qml +++ b/UIControls/qml/QoAComboBox.qml @@ -29,8 +29,10 @@ Basic.ComboBox { indicator: Image { id: dropdownIcon + x: control.width - width - 10 y: control.topPadding + (control.availableHeight - height) / 2 + width: 12 height: 8 source: palette.window.hslLightness > 0.5 @@ -101,6 +103,8 @@ Basic.ComboBox { implicitHeight: contentHeight model: control.popup.visible ? control.delegateModel : null currentIndex: control.highlightedIndex + boundsBehavior: ListView.StopAtBounds + highlightMoveDuration: 0 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded diff --git a/settings/ChatAssistantSettings.cpp b/settings/ChatAssistantSettings.cpp index 9537997..844b8ee 100644 --- a/settings/ChatAssistantSettings.cpp +++ b/settings/ChatAssistantSettings.cpp @@ -68,6 +68,10 @@ ChatAssistantSettings::ChatAssistantSettings() enableChatInNavigationPanel.setLabelText(Tr::tr("Enable chat in navigation panel")); enableChatInNavigationPanel.setDefaultValue(false); + enableChatTools.setSettingsKey(Constants::CA_ENABLE_CHAT_TOOLS); + enableChatTools.setLabelText(Tr::tr("Enable tools/function calling")); + enableChatTools.setToolTip(Tr::tr("When enabled, AI can use tools to read files, search project, and build code")); + enableChatTools.setDefaultValue(false); // General Parameters Settings temperature.setSettingsKey(Constants::CA_TEMPERATURE); @@ -146,10 +150,11 @@ ChatAssistantSettings::ChatAssistantSettings() // Extended Thinking Settings enableThinkingMode.setSettingsKey(Constants::CA_ENABLE_THINKING_MODE); - enableThinkingMode.setLabelText(Tr::tr("Enable extended thinking mode (Claude, Ollama).\n Temperature is 1.0 accordingly API requirement for Claude")); + enableThinkingMode.setLabelText(Tr::tr("Enable extended thinking mode.")); enableThinkingMode.setToolTip( - Tr::tr("Enable extended thinking mode for complex reasoning tasks. " - "This provides step-by-step reasoning before the final answer. ")); + Tr::tr("Enable extended thinking mode for complex reasoning tasks." + "This provides step-by-step reasoning before the final answer." + "Temperature is 1.0 accordingly API requirement")); enableThinkingMode.setDefaultValue(false); thinkingBudgetTokens.setSettingsKey(Constants::CA_THINKING_BUDGET_TOKENS); @@ -283,6 +288,14 @@ ChatAssistantSettings::ChatAssistantSettings() enableChatInBottomToolBar, enableChatInNavigationPanel}}, Space{8}, + Group{ + title(Tr::tr("Tools")), + Column{enableChatTools}}, + Space{8}, + Group{ + title(Tr::tr("Extended Thinking (if provider/model supports)")), + Column{enableThinkingMode, Row{thinkingGrid, Stretch{1}}}}, + Space{8}, Group{ title(Tr::tr("General Parameters")), Row{genGrid, Stretch{1}}, @@ -297,9 +310,6 @@ ChatAssistantSettings::ChatAssistantSettings() systemPrompt, }}, Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}}, - Group{ - title(Tr::tr("Extended Thinking (Claude, Ollama)")), - Column{enableThinkingMode, Row{thinkingGrid, Stretch{1}}}}, Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}}, Stretch{1}}; }); @@ -343,6 +353,7 @@ void ChatAssistantSettings::resetSettingsToDefaults() resetAspect(thinkingBudgetTokens); resetAspect(thinkingMaxTokens); resetAspect(linkOpenFiles); + resetAspect(enableChatTools); resetAspect(textFontFamily); resetAspect(codeFontFamily); resetAspect(textFontSize); diff --git a/settings/ChatAssistantSettings.hpp b/settings/ChatAssistantSettings.hpp index e7e9283..a3c890a 100644 --- a/settings/ChatAssistantSettings.hpp +++ b/settings/ChatAssistantSettings.hpp @@ -38,6 +38,7 @@ public: Utils::BoolAspect autosave{this}; Utils::BoolAspect enableChatInBottomToolBar{this}; Utils::BoolAspect enableChatInNavigationPanel{this}; + Utils::BoolAspect enableChatTools{this}; // General Parameters Settings Utils::DoubleAspect temperature{this}; diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index a877e78..99cfff5 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -102,6 +102,7 @@ const char CC_CUSTOM_LANGUAGES[] = "QodeAssist.ccCustomLanguages"; const char CA_ENABLE_CHAT_IN_BOTTOM_TOOLBAR[] = "QodeAssist.caEnableChatInBottomToolbar"; const char CA_ENABLE_CHAT_IN_NAVIGATION_PANEL[] = "QodeAssist.caEnableChatInNavigationPanel"; +const char CA_ENABLE_CHAT_TOOLS[] = "QodeAssist.caEnableChatTools"; const char CA_USE_TOOLS[] = "QodeAssist.caUseTools"; const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead"; const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite"; diff --git a/settings/ToolsSettings.cpp b/settings/ToolsSettings.cpp index be698d1..032c441 100644 --- a/settings/ToolsSettings.cpp +++ b/settings/ToolsSettings.cpp @@ -42,13 +42,6 @@ ToolsSettings::ToolsSettings() setDisplayName(Tr::tr("Tools")); - useTools.setSettingsKey(Constants::CA_USE_TOOLS); - useTools.setLabelText(Tr::tr("Enable tools")); - useTools.setToolTip(Tr::tr( - "Enable tool use capabilities for the assistant (OpenAI function calling, Claude tools " - "and etc) if plugin and provider support")); - useTools.setDefaultValue(true); - allowFileSystemRead.setSettingsKey(Constants::CA_ALLOW_FILE_SYSTEM_READ); allowFileSystemRead.setLabelText(Tr::tr("Allow File System Read Access for tools")); allowFileSystemRead.setToolTip( @@ -120,8 +113,6 @@ ToolsSettings::ToolsSettings() Group{ title(Tr::tr("Tool Settings")), Column{ - useTools, - Space{8}, allowFileSystemRead, allowFileSystemWrite, allowAccessOutsideProject @@ -158,7 +149,6 @@ void ToolsSettings::resetSettingsToDefaults() QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { - resetAspect(useTools); resetAspect(allowFileSystemRead); resetAspect(allowFileSystemWrite); resetAspect(allowAccessOutsideProject); diff --git a/settings/ToolsSettings.hpp b/settings/ToolsSettings.hpp index 5fa2bf3..6910822 100644 --- a/settings/ToolsSettings.hpp +++ b/settings/ToolsSettings.hpp @@ -32,7 +32,6 @@ public: ButtonAspect resetToDefaults{this}; - Utils::BoolAspect useTools{this}; Utils::BoolAspect allowFileSystemRead{this}; Utils::BoolAspect allowFileSystemWrite{this}; Utils::BoolAspect allowAccessOutsideProject{this};