refactor: Change top bar layout and tools/thinking settings

This commit is contained in:
Petr Mironychev
2025-11-27 00:39:37 +01:00
parent b18ef4c400
commit 85a7bba90e
16 changed files with 274 additions and 241 deletions

View File

@ -49,6 +49,8 @@ qt_add_qml_module(QodeAssistChatView
icons/reject-changes-button.svg icons/reject-changes-button.svg
icons/thinking-icon-on.svg icons/thinking-icon-on.svg
icons/thinking-icon-off.svg icons/thinking-icon-off.svg
icons/tools-icon-on.svg
icons/tools-icon-off.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp

View File

@ -37,14 +37,14 @@
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "ToolsSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "ProvidersManager.hpp"
#include "ToolsSettings.hpp"
#include "context/ChangesManager.h" #include "context/ChangesManager.h"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
#include "llmcore/RulesLoader.hpp" #include "llmcore/RulesLoader.hpp"
#include "ProvidersManager.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@ -66,18 +66,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect( connect(
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged); &settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
connect(&settings.caProvider, &Utils::BaseAspect::changed, this, [this]() { connect(&settings.caProvider, &Utils::BaseAspect::changed, this, [this]() {
auto &settings = Settings::generalSettings(); auto &settings = Settings::generalSettings();
m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(), m_currentConfiguration
settings.caModel.value()); = QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
emit currentConfigurationChanged(); emit currentConfigurationChanged();
}); });
connect(&settings.caModel, &Utils::BaseAspect::changed, this, [this]() { connect(&settings.caModel, &Utils::BaseAspect::changed, this, [this]() {
auto &settings = Settings::generalSettings(); auto &settings = Settings::generalSettings();
m_currentConfiguration = QString("%1 - %2").arg(settings.caProvider.value(), m_currentConfiguration
settings.caModel.value()); = QString("%1 - %2").arg(settings.caProvider.value(), settings.caModel.value());
emit currentConfigurationChanged(); emit currentConfigurationChanged();
}); });
@ -97,7 +97,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::updateInputTokensCount); &ChatRootView::updateInputTokensCount);
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
setRecentFilePath(QString{}); setRecentFilePath(QString{});
m_currentMessageRequestId.clear(); m_currentMessageRequestId.clear();
updateCurrentMessageEditsStats(); updateCurrentMessageEditsStats();
@ -161,41 +161,42 @@ ChatRootView::ChatRootView(QQuickItem *parent)
m_lastErrorMessage = error; m_lastErrorMessage = error;
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
}); });
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) { connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
if (!m_currentMessageRequestId.isEmpty()) { 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; m_currentMessageRequestId = requestId;
LOG_MESSAGE(QString("New message request started: %1").arg(requestId)); LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
updateCurrentMessageEditsStats(); updateCurrentMessageEditsStats();
}); });
connect( connect(
&Context::ChangesManager::instance(), &Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditAdded, &Context::ChangesManager::fileEditAdded,
this, this,
[this](const QString &) { updateCurrentMessageEditsStats(); }); [this](const QString &) { updateCurrentMessageEditsStats(); });
connect( connect(
&Context::ChangesManager::instance(), &Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied, &Context::ChangesManager::fileEditApplied,
this, this,
[this](const QString &) { updateCurrentMessageEditsStats(); }); [this](const QString &) { updateCurrentMessageEditsStats(); });
connect( connect(
&Context::ChangesManager::instance(), &Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditRejected, &Context::ChangesManager::fileEditRejected,
this, this,
[this](const QString &) { updateCurrentMessageEditsStats(); }); [this](const QString &) { updateCurrentMessageEditsStats(); });
connect( connect(
&Context::ChangesManager::instance(), &Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditUndone, &Context::ChangesManager::fileEditUndone,
this, this,
[this](const QString &) { updateCurrentMessageEditsStats(); }); [this](const QString &) { updateCurrentMessageEditsStats(); });
connect( connect(
&Context::ChangesManager::instance(), &Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditArchived, &Context::ChangesManager::fileEditArchived,
@ -212,21 +213,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::refreshRules); &ChatRootView::refreshRules);
QSettings appSettings; connect(
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool(); &Settings::chatAssistantSettings().enableChatTools,
m_isThinkingMode = Settings::chatAssistantSettings().enableThinkingMode(); &Utils::BaseAspect::changed,
this,
&ChatRootView::useToolsChanged);
connect( connect(
&Settings::chatAssistantSettings().enableThinkingMode, &Settings::chatAssistantSettings().enableThinkingMode,
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
this, this,
[this]() { setIsThinkingMode(Settings::chatAssistantSettings().enableThinkingMode()); }); &ChatRootView::useThinkingChanged);
connect(
&Settings::toolsSettings().useTools,
&Utils::BaseAspect::changed,
this,
&ChatRootView::toolsSupportEnabledChanged);
connect( connect(
&Settings::generalSettings().caProvider, &Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed, &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(); clearAttachmentFiles();
setRequestProgressStatus(true); setRequestProgressStatus(true);
} }
@ -342,7 +341,7 @@ void ChatRootView::loadHistory(const QString &filePath)
} else { } else {
setRecentFilePath(filePath); setRecentFilePath(filePath);
} }
m_currentMessageRequestId.clear(); m_currentMessageRequestId.clear();
updateInputTokensCount(); updateInputTokensCount();
updateCurrentMessageEditsStats(); updateCurrentMessageEditsStats();
@ -410,7 +409,8 @@ QString ChatRootView::getSuggestedFileName() const
shortMessage = firstMessage.split('\n').first().simplified().left(30); shortMessage = firstMessage.split('\n').first().simplified().left(30);
if (shortMessage.isEmpty()) { 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()) { if (!images.isEmpty()) {
shortMessage = "image_chat"; shortMessage = "image_chat";
} }
@ -447,7 +447,8 @@ QString ChatRootView::getAutosaveFilePath() const
return QDir(dir).filePath(getSuggestedFileName() + ".json"); 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()) { if (!m_recentFilePath.isEmpty()) {
return m_recentFilePath; return m_recentFilePath;
@ -505,7 +506,7 @@ void ChatRootView::addFilesToAttachList(const QStringList &filePaths)
filesAdded = true; filesAdded = true;
} }
} }
if (filesAdded) { if (filesAdded) {
emit attachmentFilesChanged(); emit attachmentFilesChanged();
} }
@ -541,26 +542,27 @@ void ChatRootView::addFilesToLinkList(const QStringList &filePaths)
bool filesAdded = false; bool filesAdded = false;
QStringList imageFiles; QStringList imageFiles;
for (const QString &filePath : filePaths) { for (const QString &filePath : filePaths) {
if (isImageFile(filePath)) { if (isImageFile(filePath)) {
imageFiles.append(filePath); imageFiles.append(filePath);
continue; continue;
} }
if (!m_linkedFiles.contains(filePath)) { if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath); m_linkedFiles.append(filePath);
filesAdded = true; filesAdded = true;
} }
} }
if (!imageFiles.isEmpty()) { if (!imageFiles.isEmpty()) {
addFilesToAttachList(imageFiles); 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(); emit lastInfoMessageChanged();
} }
if (filesAdded) { if (filesAdded) {
emit linkedFilesChanged(); emit linkedFilesChanged();
} }
@ -867,43 +869,26 @@ void ChatRootView::refreshRules()
emit activeRulesCountChanged(); 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) { Settings::chatAssistantSettings().enableChatTools.setValue(enabled);
m_isAgentMode = newIsAgentMode; Settings::chatAssistantSettings().writeSettings();
QSettings settings;
settings.setValue("QodeAssist/Chat/AgentMode", newIsAgentMode);
emit isAgentModeChanged();
}
} }
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) { Settings::chatAssistantSettings().enableThinkingMode.setValue(enabled);
m_isThinkingMode = newIsThinkingMode; Settings::chatAssistantSettings().writeSettings();
Settings::chatAssistantSettings().enableThinkingMode.setValue(newIsThinkingMode);
Settings::chatAssistantSettings().writeSettings();
emit isThinkingModeChanged();
}
}
bool ChatRootView::toolsSupportEnabled() const
{
return Settings::toolsSettings().useTools();
} }
void ChatRootView::applyFileEdit(const QString &editId) void ChatRootView::applyFileEdit(const QString &editId)
@ -912,13 +897,13 @@ void ChatRootView::applyFileEdit(const QString &editId)
if (Context::ChangesManager::instance().applyFileEdit(editId)) { if (Context::ChangesManager::instance().applyFileEdit(editId)) {
m_lastInfoMessage = QString("File edit applied successfully"); m_lastInfoMessage = QString("File edit applied successfully");
emit lastInfoMessageChanged(); emit lastInfoMessageChanged();
updateFileEditStatus(editId, "applied"); updateFileEditStatus(editId, "applied");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty() m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to apply file edit") ? QString("Failed to apply file edit")
: QString("Failed to apply file edit: %1").arg(edit.statusMessage); : QString("Failed to apply file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
} }
} }
@ -929,13 +914,13 @@ void ChatRootView::rejectFileEdit(const QString &editId)
if (Context::ChangesManager::instance().rejectFileEdit(editId)) { if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
m_lastInfoMessage = QString("File edit rejected"); m_lastInfoMessage = QString("File edit rejected");
emit lastInfoMessageChanged(); emit lastInfoMessageChanged();
updateFileEditStatus(editId, "rejected"); updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty() m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to reject file edit") ? QString("Failed to reject file edit")
: QString("Failed to reject file edit: %1").arg(edit.statusMessage); : QString("Failed to reject file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
} }
} }
@ -946,13 +931,13 @@ void ChatRootView::undoFileEdit(const QString &editId)
if (Context::ChangesManager::instance().undoFileEdit(editId)) { if (Context::ChangesManager::instance().undoFileEdit(editId)) {
m_lastInfoMessage = QString("File edit undone successfully"); m_lastInfoMessage = QString("File edit undone successfully");
emit lastInfoMessageChanged(); emit lastInfoMessageChanged();
updateFileEditStatus(editId, "rejected"); updateFileEditStatus(editId, "rejected");
} else { } else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty() m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to undo file edit") ? QString("Failed to undo file edit")
: QString("Failed to undo file edit: %1").arg(edit.statusMessage); : QString("Failed to undo file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
} }
} }
@ -960,37 +945,36 @@ void ChatRootView::undoFileEdit(const QString &editId)
void ChatRootView::openFileEditInEditor(const QString &editId) void ChatRootView::openFileEditInEditor(const QString &editId)
{ {
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId)); LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (edit.editId.isEmpty()) { if (edit.editId.isEmpty()) {
m_lastErrorMessage = QString("File edit not found: %1").arg(editId); m_lastErrorMessage = QString("File edit not found: %1").arg(editId);
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
return; return;
} }
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath); Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
Core::IEditor *editor = Core::EditorManager::openEditor(filePath); Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
if (!editor) { if (!editor) {
m_lastErrorMessage = QString("Failed to open file in editor: %1").arg(edit.filePath); m_lastErrorMessage = QString("Failed to open file in editor: %1").arg(edit.filePath);
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
return; return;
} }
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor); auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
if (textEditor && textEditor->editorWidget()) { if (textEditor && textEditor->editorWidget()) {
QTextDocument *doc = textEditor->editorWidget()->document(); QTextDocument *doc = textEditor->editorWidget()->document();
if (doc) { if (doc) {
QString currentContent = doc->toPlainText(); QString currentContent = doc->toPlainText();
int position = -1; int position = -1;
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) { if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
position = currentContent.indexOf(edit.newContent); position = currentContent.indexOf(edit.newContent);
} } else if (!edit.oldContent.isEmpty()) {
else if (!edit.oldContent.isEmpty()) {
position = currentContent.indexOf(edit.oldContent); position = currentContent.indexOf(edit.oldContent);
} }
if (position >= 0) { if (position >= 0) {
QTextCursor cursor(doc); QTextCursor cursor(doc);
cursor.setPosition(position); cursor.setPosition(position);
@ -999,7 +983,7 @@ void ChatRootView::openFileEditInEditor(const QString &editId)
} }
} }
} }
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath)); 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) { for (int i = 0; i < messages.size(); ++i) {
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) { if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
QString content = messages[i].content; QString content = messages[i].content;
const QString marker = "QODEASSIST_FILE_EDIT:"; const QString marker = "QODEASSIST_FILE_EDIT:";
int markerPos = content.indexOf(marker); int markerPos = content.indexOf(marker);
QString jsonStr = content; QString jsonStr = content;
if (markerPos >= 0) { if (markerPos >= 0) {
jsonStr = content.mid(markerPos + marker.length()); jsonStr = content.mid(markerPos + marker.length());
} }
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8()); QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) { if (doc.isObject()) {
QJsonObject obj = doc.object(); QJsonObject obj = doc.object();
obj["status"] = status; obj["status"] = status;
auto edit = Context::ChangesManager::instance().getFileEdit(editId); auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (!edit.statusMessage.isEmpty()) { if (!edit.statusMessage.isEmpty()) {
obj["status_message"] = edit.statusMessage; 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); m_chatModel->updateMessageContent(editId, updatedContent);
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status)); LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
} }
break; break;
} }
} }
updateCurrentMessageEditsStats(); updateCurrentMessageEditsStats();
} }
@ -1046,37 +1032,39 @@ void ChatRootView::applyAllFileEditsForCurrentMessage()
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
return; return;
} }
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentMessageRequestId)); LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentMessageRequestId));
QString errorMsg; QString errorMsg;
bool success = Context::ChangesManager::instance() bool success = Context::ChangesManager::instance()
.reapplyAllEditsForRequest(m_currentMessageRequestId, &errorMsg); .reapplyAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
if (success) { if (success) {
m_lastInfoMessage = QString("All file edits applied successfully"); m_lastInfoMessage = QString("All file edits applied successfully");
emit lastInfoMessageChanged(); emit lastInfoMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId); auto edits = Context::ChangesManager::instance().getEditsForRequest(
m_currentMessageRequestId);
for (const auto &edit : edits) { for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) { if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied"); updateFileEditStatus(edit.editId, "applied");
} }
} }
} else { } else {
m_lastErrorMessage = errorMsg.isEmpty() m_lastErrorMessage = errorMsg.isEmpty()
? QString("Failed to apply some file edits") ? QString("Failed to apply some file edits")
: QString("Failed to apply some file edits:\n%1").arg(errorMsg); : QString("Failed to apply some file edits:\n%1").arg(errorMsg);
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId); auto edits = Context::ChangesManager::instance().getEditsForRequest(
m_currentMessageRequestId);
for (const auto &edit : edits) { for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) { if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied"); updateFileEditStatus(edit.editId, "applied");
} }
} }
} }
updateCurrentMessageEditsStats(); updateCurrentMessageEditsStats();
} }
@ -1087,45 +1075,47 @@ void ChatRootView::undoAllFileEditsForCurrentMessage()
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
return; return;
} }
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentMessageRequestId)); LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentMessageRequestId));
QString errorMsg; QString errorMsg;
bool success = Context::ChangesManager::instance() bool success = Context::ChangesManager::instance()
.undoAllEditsForRequest(m_currentMessageRequestId, &errorMsg); .undoAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
if (success) { if (success) {
m_lastInfoMessage = QString("All file edits undone successfully"); m_lastInfoMessage = QString("All file edits undone successfully");
emit lastInfoMessageChanged(); emit lastInfoMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId); auto edits = Context::ChangesManager::instance().getEditsForRequest(
m_currentMessageRequestId);
for (const auto &edit : edits) { for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) { if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected"); updateFileEditStatus(edit.editId, "rejected");
} }
} }
} else { } else {
m_lastErrorMessage = errorMsg.isEmpty() m_lastErrorMessage = errorMsg.isEmpty()
? QString("Failed to undo some file edits") ? QString("Failed to undo some file edits")
: QString("Failed to undo some file edits:\n%1").arg(errorMsg); : QString("Failed to undo some file edits:\n%1").arg(errorMsg);
emit lastErrorMessageChanged(); emit lastErrorMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId); auto edits = Context::ChangesManager::instance().getEditsForRequest(
m_currentMessageRequestId);
for (const auto &edit : edits) { for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) { if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected"); updateFileEditStatus(edit.editId, "rejected");
} }
} }
} }
updateCurrentMessageEditsStats(); updateCurrentMessageEditsStats();
} }
void ChatRootView::updateCurrentMessageEditsStats() void ChatRootView::updateCurrentMessageEditsStats()
{ {
if (m_currentMessageRequestId.isEmpty()) { if (m_currentMessageRequestId.isEmpty()) {
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 || if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0
m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) { || m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
m_currentMessageTotalEdits = 0; m_currentMessageTotalEdits = 0;
m_currentMessageAppliedEdits = 0; m_currentMessageAppliedEdits = 0;
m_currentMessagePendingEdits = 0; m_currentMessagePendingEdits = 0;
@ -1134,14 +1124,14 @@ void ChatRootView::updateCurrentMessageEditsStats()
} }
return; return;
} }
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId); auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
int total = edits.size(); int total = edits.size();
int applied = 0; int applied = 0;
int pending = 0; int pending = 0;
int rejected = 0; int rejected = 0;
for (const auto &edit : edits) { for (const auto &edit : edits) {
switch (edit.status) { switch (edit.status) {
case Context::ChangesManager::Applied: case Context::ChangesManager::Applied:
@ -1158,7 +1148,7 @@ void ChatRootView::updateCurrentMessageEditsStats()
break; break;
} }
} }
bool changed = false; bool changed = false;
if (m_currentMessageTotalEdits != total) { if (m_currentMessageTotalEdits != total) {
m_currentMessageTotalEdits = total; m_currentMessageTotalEdits = total;
@ -1176,10 +1166,14 @@ void ChatRootView::updateCurrentMessageEditsStats()
m_currentMessageRejectedEdits = rejected; m_currentMessageRejectedEdits = rejected;
changed = true; changed = true;
} }
if (changed) { if (changed) {
LOG_MESSAGE(QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4") LOG_MESSAGE(
.arg(total).arg(applied).arg(pending).arg(rejected)); QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
.arg(total)
.arg(applied)
.arg(pending)
.arg(rejected));
emit currentMessageEditsStatsChanged(); emit currentMessageEditsStatsChanged();
} }
} }
@ -1268,9 +1262,7 @@ bool ChatRootView::hasImageAttachments(const QStringList &attachments) const
bool ChatRootView::isImageFile(const QString &filePath) const bool ChatRootView::isImageFile(const QString &filePath) const
{ {
static const QSet<QString> imageExtensions = { static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"
};
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
return imageExtensions.contains(fileInfo.suffix().toLower()); return imageExtensions.contains(fileInfo.suffix().toLower());
@ -1318,7 +1310,8 @@ void ChatRootView::applyConfiguration(const QString &configName)
settings.caModel.setValue(config.model); settings.caModel.setValue(config.model);
settings.caTemplate.setValue(config.templateName); settings.caTemplate.setValue(config.templateName);
settings.caUrl.setValue(config.url); 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.caCustomEndpoint.setValue(config.customEndpoint);
settings.writeSettings(); settings.writeSettings();

View File

@ -48,10 +48,8 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL) Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL) Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL) Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL) Q_PROPERTY(bool useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL) Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL) Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
@ -127,11 +125,10 @@ public:
Q_INVOKABLE QString getRuleContent(int index); Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules(); Q_INVOKABLE void refreshRules();
bool isAgentMode() const; bool useTools() const;
void setIsAgentMode(bool newIsAgentMode); void setUseTools(bool enabled);
bool isThinkingMode() const; bool useThinking() const;
void setIsThinkingMode(bool newIsThinkingMode); void setUseThinking(bool enabled);
bool toolsSupportEnabled() const;
Q_INVOKABLE void applyFileEdit(const QString &editId); Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId); Q_INVOKABLE void rejectFileEdit(const QString &editId);
@ -184,9 +181,8 @@ signals:
void activeRulesChanged(); void activeRulesChanged();
void activeRulesCountChanged(); void activeRulesCountChanged();
void isAgentModeChanged(); void useToolsChanged();
void isThinkingModeChanged(); void useThinkingChanged();
void toolsSupportEnabledChanged();
void currentMessageEditsStatsChanged(); void currentMessageEditsStatsChanged();
void isThinkingSupportChanged(); void isThinkingSupportChanged();
@ -214,8 +210,6 @@ private:
bool m_isRequestInProgress; bool m_isRequestInProgress;
QString m_lastErrorMessage; QString m_lastErrorMessage;
QVariantList m_activeRules; QVariantList m_activeRules;
bool m_isAgentMode;
bool m_isThinkingMode;
QString m_currentMessageRequestId; QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0}; int m_currentMessageTotalEdits{0};

View File

@ -43,12 +43,12 @@
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "ToolsSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProvidersManager.hpp" #include "ProvidersManager.hpp"
#include "RequestConfig.hpp" #include "RequestConfig.hpp"
#include <context/ChangesManager.h> #include "ToolsSettings.hpp"
#include <RulesLoader.hpp> #include <RulesLoader.hpp>
#include <context/ChangesManager.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@ -69,16 +69,17 @@ void ClientInterface::sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments, const QList<QString> &attachments,
const QList<QString> &linkedFiles, const QList<QString> &linkedFiles,
bool useAgentMode) bool useTools,
bool useThinking)
{ {
cancelRequest(); cancelRequest();
m_accumulatedResponses.clear(); m_accumulatedResponses.clear();
Context::ChangesManager::instance().archiveAllNonArchivedEdits(); Context::ChangesManager::instance().archiveAllNonArchivedEdits();
QList<QString> imageFiles; QList<QString> imageFiles;
QList<QString> textFiles; QList<QString> textFiles;
for (const QString &filePath : attachments) { for (const QString &filePath : attachments) {
if (isImageFile(filePath)) { if (isImageFile(filePath)) {
imageFiles.append(filePath); imageFiles.append(filePath);
@ -88,7 +89,7 @@ void ClientInterface::sendMessage(
} }
auto attachFiles = m_contextManager->getContentFiles(textFiles); auto attachFiles = m_contextManager->getContentFiles(textFiles);
QList<ChatModel::ImageAttachment> imageAttachments; QList<ChatModel::ImageAttachment> imageAttachments;
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) { if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const QString &imagePath : imageFiles) { for (const QString &imagePath : imageFiles) {
@ -96,23 +97,25 @@ void ClientInterface::sendMessage(
if (base64Data.isEmpty()) { if (base64Data.isEmpty()) {
continue; continue;
} }
QString storedPath; QString storedPath;
QFileInfo fileInfo(imagePath); QFileInfo fileInfo(imagePath);
if (ChatSerializer::saveImageToStorage(m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) { if (ChatSerializer::saveImageToStorage(
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
ChatModel::ImageAttachment imageAttachment; ChatModel::ImageAttachment imageAttachment;
imageAttachment.fileName = fileInfo.fileName(); imageAttachment.fileName = fileInfo.fileName();
imageAttachment.storedPath = storedPath; imageAttachment.storedPath = storedPath;
imageAttachment.mediaType = getMediaTypeForImage(imagePath); imageAttachment.mediaType = getMediaTypeForImage(imagePath);
imageAttachments.append(imageAttachment); imageAttachments.append(imageAttachment);
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath)); LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
} }
} }
} else if (!imageFiles.isEmpty()) { } 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); m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles, imageAttachments);
auto &chatAssistantSettings = Settings::chatAssistantSettings(); auto &chatAssistantSettings = Settings::chatAssistantSettings();
@ -135,7 +138,7 @@ void ClientInterface::sendMessage(
LLMCore::ContextData context; LLMCore::ContextData context;
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode; const bool isToolsEnabled = useTools;
if (chatAssistantSettings.useSystemPrompt()) { if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt(); QString systemPrompt = chatAssistantSettings.systemPrompt();
@ -144,8 +147,9 @@ void ClientInterface::sendMessage(
if (project) { if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName()); 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 target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) { if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt += QString("\n# Active Build directory: %1") 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) { if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
continue; continue;
} }
LLMCore::Message apiMessage; LLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant"; apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content; apiMessage.content = msg.content;
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking); apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted; apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature; apiMessage.signature = msg.signature;
if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) { if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images); auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) { if (!apiImages.isEmpty()) {
apiMessage.images = apiImages; apiMessage.images = apiImages;
} }
} }
messages.append(apiMessage); messages.append(apiMessage);
} }
if (!imageFiles.isEmpty() && !provider->supportImage()) { if (!imageFiles.isEmpty() && !provider->supportImage()) {
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored") LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size()))); .arg(provider->name(), QString::number(imageFiles.size())));
} }
context.history = messages; context.history = messages;
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
@ -224,14 +228,14 @@ void ClientInterface::sendMessage(
promptTemplate, promptTemplate,
context, context,
LLMCore::RequestType::Chat, LLMCore::RequestType::Chat,
isToolsEnabled, useTools,
Settings::chatAssistantSettings().enableThinkingMode()); useThinking);
QString requestId = QUuid::createUuid().toString(); QString requestId = QUuid::createUuid().toString();
QJsonObject request{{"id", requestId}}; QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider}; m_activeRequests[requestId] = {request, provider};
emit requestStarted(requestId); emit requestStarted(requestId);
connect( connect(
@ -395,14 +399,14 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
const RequestContext &ctx = it.value(); const RequestContext &ctx = it.value();
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId]; QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
QString applyError; QString applyError;
bool applySuccess = Context::ChangesManager::instance() bool applySuccess
.applyPendingEditsForRequest(requestId, &applyError); = Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
if (!applySuccess) { if (!applySuccess) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2") LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError)); .arg(requestId, applyError));
} }
LOG_MESSAGE( LOG_MESSAGE(
@ -443,35 +447,32 @@ void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
bool ClientInterface::isImageFile(const QString &filePath) const bool ClientInterface::isImageFile(const QString &filePath) const
{ {
static const QSet<QString> imageExtensions = { static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"
};
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
QString extension = fileInfo.suffix().toLower(); QString extension = fileInfo.suffix().toLower();
return imageExtensions.contains(extension); return imageExtensions.contains(extension);
} }
QString ClientInterface::getMediaTypeForImage(const QString &filePath) const QString ClientInterface::getMediaTypeForImage(const QString &filePath) const
{ {
static const QHash<QString, QString> mediaTypes = { static const QHash<QString, QString> mediaTypes
{"png", "image/png"}, = {{"png", "image/png"},
{"jpg", "image/jpeg"}, {"jpg", "image/jpeg"},
{"jpeg", "image/jpeg"}, {"jpeg", "image/jpeg"},
{"gif", "image/gif"}, {"gif", "image/gif"},
{"webp", "image/webp"}, {"webp", "image/webp"},
{"bmp", "image/bmp"}, {"bmp", "image/bmp"},
{"svg", "image/svg+xml"} {"svg", "image/svg+xml"}};
};
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
QString extension = fileInfo.suffix().toLower(); QString extension = fileInfo.suffix().toLower();
if (mediaTypes.contains(extension)) { if (mediaTypes.contains(extension)) {
return mediaTypes[extension]; return mediaTypes[extension];
} }
QMimeDatabase mimeDb; QMimeDatabase mimeDb;
QMimeType mimeType = mimeDb.mimeTypeForFile(filePath); QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
return mimeType.name(); 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)); LOG_MESSAGE(QString("Failed to open image file: %1").arg(filePath));
return QString(); return QString();
} }
QByteArray imageData = file.readAll(); QByteArray imageData = file.readAll();
file.close(); file.close();
return imageData.toBase64(); return imageData.toBase64();
} }
QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const QVector<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const
{ {
QVector<LLMCore::ImageAttachment> apiImages; QVector<LLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) { for (const auto &storedImage : storedImages) {
QString base64Data = ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath); QString base64Data
= ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath);
if (base64Data.isEmpty()) { if (base64Data.isEmpty()) {
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath)); LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
continue; continue;
} }
LLMCore::ImageAttachment apiImage; LLMCore::ImageAttachment apiImage;
apiImage.data = base64Data; apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType; apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false; apiImage.isUrl = false;
apiImages.append(apiImage); apiImages.append(apiImage);
} }
return apiImages; return apiImages;
} }

View File

@ -43,7 +43,8 @@ public:
const QString &message, const QString &message,
const QList<QString> &attachments = {}, const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {}, const QList<QString> &linkedFiles = {},
bool useAgentMode = false); bool useTools = false,
bool useThinking = false);
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();

View File

@ -1,4 +1,4 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/> <path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black" fill-opacity="0.6"/>
<path d="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/> <path d="M6 35L38 6" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,11 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_82_71)">
<path d="M10.7777 0.0435181C14.2316 -0.253961 17.6161 0.979215 20.0629 3.42633C23.4139 6.77767 24.3012 11.6719 22.7299 15.8433C22.9016 15.988 23.0706 16.1419 23.2377 16.3072L42.2221 34.2203C42.2288 34.2268 42.2353 34.2344 42.2426 34.2408C44.4752 36.4735 44.4752 40.1064 42.2426 42.3394C40.0096 44.5717 36.4035 44.5446 34.1713 42.3121C34.1617 42.3031 34.1528 42.2937 34.144 42.2838L16.3871 23.1519C16.2254 22.9894 16.0746 22.8196 15.933 22.6451C11.7604 24.2194 6.86327 23.3335 3.50919 19.98C1.06298 17.5327 -0.171482 14.1483 0.126373 10.6949C0.160109 10.3034 0.41818 9.96685 0.78653 9.83258C1.15602 9.69759 1.57009 9.78945 1.84805 10.067L7.53555 15.7535L13.8402 13.7574L15.8363 7.4527L10.1488 1.7652C9.87057 1.48716 9.77945 1.07345 9.91348 0.704651C10.0489 0.335072 10.3852 0.0774496 10.7777 0.0435181ZM37.3656 34.7496L37.3129 34.9302L37.1586 35.4673L36.8363 35.5679L36.6195 35.2047L36.4623 34.942L36.2357 35.148L35.725 35.6148L35.5746 35.7525L35.6791 35.9283L35.9184 36.3287L35.7104 36.6548L35.1742 36.5543L34.9408 36.5093L34.8852 36.7418L34.7572 37.275L34.7123 37.4644L34.8842 37.5543L35.3891 37.8179V38.1802L34.8842 38.4449L34.7123 38.5347L34.7572 38.7242L34.8852 39.2574L34.9408 39.4898L35.1742 39.4449L35.7104 39.3433L35.9184 39.6695L35.6791 40.0709L35.5746 40.2466L35.725 40.3843L36.2357 40.8511L36.4623 41.0572L36.6195 40.7945L36.8363 40.4302L37.1586 40.5308L37.3129 41.0689L37.3656 41.2496H38.6352L38.6879 41.0689L38.8412 40.5308L39.1635 40.4302L39.3813 40.7945L39.5385 41.0572L39.765 40.8511L40.2758 40.3843L40.4262 40.2466L40.3217 40.0709L40.0815 39.6695L40.2895 39.3433L40.8266 39.4449L41.06 39.4898L41.1156 39.2574L41.2436 38.7242L41.2885 38.5347L41.1166 38.4449L40.6117 38.1802V37.8179L41.1166 37.5543L41.2885 37.4644L41.2436 37.275L41.1156 36.7418L41.06 36.5093L40.8266 36.5543L40.2895 36.6548L40.0815 36.3287L40.3217 35.9283L40.4262 35.7525L40.2758 35.6148L39.765 35.148L39.5385 34.942L39.3813 35.2047L39.1635 35.5679L38.8412 35.4673L38.6879 34.9302L38.6352 34.7496H37.3656Z" fill="black" fill-opacity="0.6"/>
<path d="M6 36L38 7" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_82_71">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,10 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_82_50)">
<path d="M10.7775 0.0441895C14.2315 -0.253375 17.6159 0.979824 20.0627 3.427C23.4135 6.77842 24.3011 11.6726 22.7297 15.844C22.9013 15.9886 23.0714 16.1416 23.2385 16.3069L42.2219 34.2209C42.2285 34.2274 42.2352 34.2341 42.2424 34.2405C44.475 36.4732 44.475 40.1061 42.2424 42.3391C40.0094 44.5715 36.4033 44.5444 34.1711 42.3118C34.1615 42.3028 34.1525 42.2934 34.1437 42.2834L16.3869 23.1516C16.2251 22.9891 16.0745 22.8193 15.9328 22.6448C11.7602 24.2191 6.86304 23.3333 3.50897 19.9797C1.06276 17.5324 -0.171773 14.148 0.12616 10.6946C0.159908 10.3029 0.418723 9.96644 0.787292 9.83228C1.15667 9.69748 1.56997 9.78926 1.84784 10.0667L7.53534 15.7532L13.84 13.7571L15.8361 7.45239L10.1486 1.76489C9.87052 1.48684 9.78022 1.07306 9.91425 0.704346C10.0498 0.334991 10.3852 0.0781082 10.7775 0.0441895ZM37.3654 34.7502L37.3127 34.9309L37.1584 35.468L36.8361 35.5686L36.6193 35.2053L36.4621 34.9426L36.2355 35.1487L35.7248 35.6155L35.5744 35.7532L35.6789 35.929L35.9182 36.3293L35.7101 36.6555L35.174 36.5549L34.9406 36.51L34.8849 36.7424L34.757 37.2756L34.7121 37.4651L34.884 37.5549L35.3889 37.8186V38.1809L34.884 38.4456L34.7121 38.5354L34.757 38.7249L34.8849 39.2581L34.9406 39.4905L35.174 39.4456L35.7101 39.344L35.9182 39.6702L35.6789 40.0715L35.5744 40.2473L35.7248 40.385L36.2355 40.8518L36.4621 41.0579L36.6193 40.7952L36.8361 40.4309L37.1584 40.5315L37.3127 41.0696L37.3654 41.2502H38.6349L38.6877 41.0696L38.841 40.5315L39.1633 40.4309L39.381 40.7952L39.5383 41.0579L39.7648 40.8518L40.2756 40.385L40.426 40.2473L40.3215 40.0715L40.0812 39.6702L40.2892 39.344L40.8264 39.4456L41.0598 39.4905L41.1154 39.2581L41.2433 38.7249L41.2883 38.5354L41.1164 38.4456L40.6115 38.1809V37.8186L41.1164 37.5549L41.2883 37.4651L41.2433 37.2756L41.1154 36.7424L41.0598 36.51L40.8264 36.5549L40.2892 36.6555L40.0812 36.3293L40.3215 35.929L40.426 35.7532L40.2756 35.6155L39.7648 35.1487L39.5383 34.9426L39.381 35.2053L39.1633 35.5686L38.841 35.468L38.6877 34.9309L38.6349 34.7502H37.3654Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_82_50">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -103,18 +103,17 @@ ChatRootView {
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
} }
agentModeSwitch { toolsButton {
checked: root.isAgentMode checked: root.useTools
enabled: root.toolsSupportEnabled onCheckedChanged: {
onToggled: { root.useTools = toolsButton.checked
root.isAgentMode = agentModeSwitch.checked
} }
} }
thinkingMode { thinkingMode {
checked: root.isThinkingMode checked: root.useThinking
enabled: root.isThinkingSupport enabled: root.isThinkingSupport
onCheckedChanged: { onCheckedChanged: {
root.isThinkingMode = thinkingMode.checked root.useThinking = thinkingMode.checked
} }
} }
configSelector { configSelector {

View File

@ -34,7 +34,7 @@ Rectangle {
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId property alias pinButton: pinButtonId
property alias rulesButton: rulesButtonId property alias rulesButton: rulesButtonId
property alias agentModeSwitch: agentModeSwitchId property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId property alias thinkingMode: thinkingModeId
property alias activeRulesCount: activeRulesCountId.text property alias activeRulesCount: activeRulesCountId.text
property alias configSelector: configSelectorId property alias configSelector: configSelectorId
@ -53,7 +53,8 @@ Rectangle {
spacing: 10 spacing: 10
Row { Row {
height: agentModeSwitchId.height id: firstRow
spacing: 10 spacing: 10
QoAButton { QoAButton {
@ -75,23 +76,44 @@ Rectangle {
: qsTr("Pin chat window to the top") : qsTr("Pin chat window to the top")
} }
QoATextSlider { QoAComboBox {
id: agentModeSwitchId 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 anchors.verticalCenter: parent.verticalCenter
leftText: "chat" checkable: true
rightText: "AI Agent" 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.visible: hovered
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: { ToolTip.text: {
if (!agentModeSwitchId.enabled) { if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings") return qsTr("Tools are disabled in General Settings")
} }
return checked return checked
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code") ? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Chat Mode: Simple conversation without tool access") : qsTr("Tools disabled: Simple conversation without tool access")
} }
} }
@ -120,7 +142,7 @@ Rectangle {
} }
Item { Item {
height: agentModeSwitchId.height height: firstRow.height
width: recentPathId.width width: recentPathId.width
Text { Text {
@ -144,7 +166,10 @@ Rectangle {
} }
RowLayout { RowLayout {
id: secondRow
Layout.preferredWidth: root.width Layout.preferredWidth: root.width
Layout.preferredHeight: firstRow.height
spacing: 10 spacing: 10
@ -239,17 +264,6 @@ Rectangle {
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold") ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
} }
QoAComboBox {
id: configSelectorId
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch AI configuration")
}
} }
} }
} }

View File

@ -29,8 +29,10 @@ Basic.ComboBox {
indicator: Image { indicator: Image {
id: dropdownIcon id: dropdownIcon
x: control.width - width - 10 x: control.width - width - 10
y: control.topPadding + (control.availableHeight - height) / 2 y: control.topPadding + (control.availableHeight - height) / 2
width: 12 width: 12
height: 8 height: 8
source: palette.window.hslLightness > 0.5 source: palette.window.hslLightness > 0.5
@ -101,6 +103,8 @@ Basic.ComboBox {
implicitHeight: contentHeight implicitHeight: contentHeight
model: control.popup.visible ? control.delegateModel : null model: control.popup.visible ? control.delegateModel : null
currentIndex: control.highlightedIndex currentIndex: control.highlightedIndex
boundsBehavior: ListView.StopAtBounds
highlightMoveDuration: 0
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded policy: ScrollBar.AsNeeded

View File

@ -68,6 +68,10 @@ ChatAssistantSettings::ChatAssistantSettings()
enableChatInNavigationPanel.setLabelText(Tr::tr("Enable chat in navigation panel")); enableChatInNavigationPanel.setLabelText(Tr::tr("Enable chat in navigation panel"));
enableChatInNavigationPanel.setDefaultValue(false); 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 // General Parameters Settings
temperature.setSettingsKey(Constants::CA_TEMPERATURE); temperature.setSettingsKey(Constants::CA_TEMPERATURE);
@ -146,10 +150,11 @@ ChatAssistantSettings::ChatAssistantSettings()
// Extended Thinking Settings // Extended Thinking Settings
enableThinkingMode.setSettingsKey(Constants::CA_ENABLE_THINKING_MODE); 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( enableThinkingMode.setToolTip(
Tr::tr("Enable extended thinking mode for complex reasoning tasks. " Tr::tr("Enable extended thinking mode for complex reasoning tasks."
"This provides step-by-step reasoning before the final answer. ")); "This provides step-by-step reasoning before the final answer."
"Temperature is 1.0 accordingly API requirement"));
enableThinkingMode.setDefaultValue(false); enableThinkingMode.setDefaultValue(false);
thinkingBudgetTokens.setSettingsKey(Constants::CA_THINKING_BUDGET_TOKENS); thinkingBudgetTokens.setSettingsKey(Constants::CA_THINKING_BUDGET_TOKENS);
@ -283,6 +288,14 @@ ChatAssistantSettings::ChatAssistantSettings()
enableChatInBottomToolBar, enableChatInBottomToolBar,
enableChatInNavigationPanel}}, enableChatInNavigationPanel}},
Space{8}, 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{ Group{
title(Tr::tr("General Parameters")), title(Tr::tr("General Parameters")),
Row{genGrid, Stretch{1}}, Row{genGrid, Stretch{1}},
@ -297,9 +310,6 @@ ChatAssistantSettings::ChatAssistantSettings()
systemPrompt, systemPrompt,
}}, }},
Group{title(Tr::tr("Ollama Settings")), Column{Row{ollamaGrid, Stretch{1}}}}, 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}}}, Group{title(Tr::tr("Chat Settings")), Row{chatViewSettingsGrid, Stretch{1}}},
Stretch{1}}; Stretch{1}};
}); });
@ -343,6 +353,7 @@ void ChatAssistantSettings::resetSettingsToDefaults()
resetAspect(thinkingBudgetTokens); resetAspect(thinkingBudgetTokens);
resetAspect(thinkingMaxTokens); resetAspect(thinkingMaxTokens);
resetAspect(linkOpenFiles); resetAspect(linkOpenFiles);
resetAspect(enableChatTools);
resetAspect(textFontFamily); resetAspect(textFontFamily);
resetAspect(codeFontFamily); resetAspect(codeFontFamily);
resetAspect(textFontSize); resetAspect(textFontSize);

View File

@ -38,6 +38,7 @@ public:
Utils::BoolAspect autosave{this}; Utils::BoolAspect autosave{this};
Utils::BoolAspect enableChatInBottomToolBar{this}; Utils::BoolAspect enableChatInBottomToolBar{this};
Utils::BoolAspect enableChatInNavigationPanel{this}; Utils::BoolAspect enableChatInNavigationPanel{this};
Utils::BoolAspect enableChatTools{this};
// General Parameters Settings // General Parameters Settings
Utils::DoubleAspect temperature{this}; Utils::DoubleAspect temperature{this};

View File

@ -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_BOTTOM_TOOLBAR[] = "QodeAssist.caEnableChatInBottomToolbar";
const char CA_ENABLE_CHAT_IN_NAVIGATION_PANEL[] = "QodeAssist.caEnableChatInNavigationPanel"; 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_USE_TOOLS[] = "QodeAssist.caUseTools";
const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead"; const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead";
const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite"; const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite";

View File

@ -42,13 +42,6 @@ ToolsSettings::ToolsSettings()
setDisplayName(Tr::tr("Tools")); 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.setSettingsKey(Constants::CA_ALLOW_FILE_SYSTEM_READ);
allowFileSystemRead.setLabelText(Tr::tr("Allow File System Read Access for tools")); allowFileSystemRead.setLabelText(Tr::tr("Allow File System Read Access for tools"));
allowFileSystemRead.setToolTip( allowFileSystemRead.setToolTip(
@ -120,8 +113,6 @@ ToolsSettings::ToolsSettings()
Group{ Group{
title(Tr::tr("Tool Settings")), title(Tr::tr("Tool Settings")),
Column{ Column{
useTools,
Space{8},
allowFileSystemRead, allowFileSystemRead,
allowFileSystemWrite, allowFileSystemWrite,
allowAccessOutsideProject allowAccessOutsideProject
@ -158,7 +149,6 @@ void ToolsSettings::resetSettingsToDefaults()
QMessageBox::Yes | QMessageBox::No); QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
resetAspect(useTools);
resetAspect(allowFileSystemRead); resetAspect(allowFileSystemRead);
resetAspect(allowFileSystemWrite); resetAspect(allowFileSystemWrite);
resetAspect(allowAccessOutsideProject); resetAspect(allowAccessOutsideProject);

View File

@ -32,7 +32,6 @@ public:
ButtonAspect resetToDefaults{this}; ButtonAspect resetToDefaults{this};
Utils::BoolAspect useTools{this};
Utils::BoolAspect allowFileSystemRead{this}; Utils::BoolAspect allowFileSystemRead{this};
Utils::BoolAspect allowFileSystemWrite{this}; Utils::BoolAspect allowFileSystemWrite{this};
Utils::BoolAspect allowAccessOutsideProject{this}; Utils::BoolAspect allowAccessOutsideProject{this};