mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2025-12-08 01:36:45 +08:00
Compare commits
7 Commits
670f81c3dd
...
6ec4a61c0c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec4a61c0c | ||
|
|
7feb088de3 | ||
|
|
627a821115 | ||
|
|
9b0ae98f02 | ||
|
|
85a7bba90e | ||
|
|
b18ef4c400 | ||
|
|
bbacdfc22a |
@ -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
|
||||
|
||||
@ -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<TextEditor::BaseTextEditor *>(editor);
|
||||
if (textEditor && textEditor->editorWidget()) {
|
||||
QTextDocument *doc = textEditor->editorWidget()->document();
|
||||
if (doc) {
|
||||
QString currentContent = doc->toPlainText();
|
||||
int position = -1;
|
||||
|
||||
|
||||
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
|
||||
position = currentContent.indexOf(edit.newContent);
|
||||
}
|
||||
else if (!edit.oldContent.isEmpty()) {
|
||||
} 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<QString> imageExtensions = {
|
||||
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"
|
||||
};
|
||||
static const QSet<QString> 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();
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
|
||||
#include "ClientInterface.hpp"
|
||||
|
||||
#include <projectexplorer/buildconfiguration.h>
|
||||
#include <projectexplorer/target.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
@ -41,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 <context/ChangesManager.h>
|
||||
#include "ToolsSettings.hpp"
|
||||
#include <RulesLoader.hpp>
|
||||
#include <context/ChangesManager.h>
|
||||
|
||||
namespace QodeAssist::Chat {
|
||||
|
||||
@ -67,16 +69,17 @@ void ClientInterface::sendMessage(
|
||||
const QString &message,
|
||||
const QList<QString> &attachments,
|
||||
const QList<QString> &linkedFiles,
|
||||
bool useAgentMode)
|
||||
bool useTools,
|
||||
bool useThinking)
|
||||
{
|
||||
cancelRequest();
|
||||
m_accumulatedResponses.clear();
|
||||
|
||||
|
||||
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||
|
||||
QList<QString> imageFiles;
|
||||
QList<QString> textFiles;
|
||||
|
||||
|
||||
for (const QString &filePath : attachments) {
|
||||
if (isImageFile(filePath)) {
|
||||
imageFiles.append(filePath);
|
||||
@ -86,7 +89,7 @@ void ClientInterface::sendMessage(
|
||||
}
|
||||
|
||||
auto attachFiles = m_contextManager->getContentFiles(textFiles);
|
||||
|
||||
|
||||
QList<ChatModel::ImageAttachment> imageAttachments;
|
||||
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||
for (const QString &imagePath : imageFiles) {
|
||||
@ -94,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();
|
||||
@ -133,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();
|
||||
@ -142,7 +147,15 @@ 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")
|
||||
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||
}
|
||||
}
|
||||
|
||||
QString projectRules
|
||||
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
|
||||
@ -165,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;
|
||||
@ -215,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(
|
||||
@ -386,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(
|
||||
@ -434,35 +447,32 @@ void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
|
||||
|
||||
bool ClientInterface::isImageFile(const QString &filePath) const
|
||||
{
|
||||
static const QSet<QString> imageExtensions = {
|
||||
"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"
|
||||
};
|
||||
|
||||
static const QSet<QString> 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<QString, QString> 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<QString, QString> 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();
|
||||
@ -475,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<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;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -43,7 +43,8 @@ public:
|
||||
const QString &message,
|
||||
const QList<QString> &attachments = {},
|
||||
const QList<QString> &linkedFiles = {},
|
||||
bool useAgentMode = false);
|
||||
bool useTools = false,
|
||||
bool useThinking = false);
|
||||
void clearMessages();
|
||||
void cancelRequest();
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<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="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||
<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-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
11
ChatView/icons/tools-icon-off.svg
Normal file
11
ChatView/icons/tools-icon-off.svg
Normal 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 |
10
ChatView/icons/tools-icon-on.svg
Normal file
10
ChatView/icons/tools-icon-on.svg
Normal 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 |
@ -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 {
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.9.1",
|
||||
"Version" : "0.9.2",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -70,4 +70,18 @@ QString ProjectUtils::findFileInProject(const QString &filename)
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString ProjectUtils::getProjectRoot()
|
||||
{
|
||||
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||
|
||||
if (!projects.isEmpty()) {
|
||||
auto project = projects.first();
|
||||
if (project) {
|
||||
return project->projectDirectory().toFSPathString();
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
|
||||
@ -52,6 +52,16 @@ public:
|
||||
* @return Absolute file path if found, empty string otherwise
|
||||
*/
|
||||
static QString findFileInProject(const QString &filename);
|
||||
|
||||
/**
|
||||
* @brief Get the project root directory
|
||||
*
|
||||
* Returns the root directory of the first open project.
|
||||
* If multiple projects are open, returns the first one.
|
||||
*
|
||||
* @return Absolute path to project root, or empty string if no project is open
|
||||
*/
|
||||
static QString getProjectRoot();
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Context
|
||||
|
||||
@ -445,8 +445,6 @@ void OllamaProvider::processStreamData(const QString &requestId, const QJsonObje
|
||||
QString thinkingDelta = messageObj["thinking"].toString();
|
||||
if (!thinkingDelta.isEmpty()) {
|
||||
message->handleThinkingDelta(thinkingDelta);
|
||||
LOG_MESSAGE(QString("OllamaProvider: Received thinking delta from message.thinking, length=%1")
|
||||
.arg(thinkingDelta.length()));
|
||||
|
||||
if (!m_thinkingStarted.contains(requestId)) {
|
||||
auto thinkingBlocks = message->getCurrentThinkingContent();
|
||||
@ -457,9 +455,6 @@ void OllamaProvider::processStreamData(const QString &requestId, const QJsonObje
|
||||
: currentThinking;
|
||||
|
||||
emit thinkingBlockReceived(requestId, displayThinking, "");
|
||||
LOG_MESSAGE(QString("Emitted initial thinking indicator for request %1, length=%2")
|
||||
.arg(requestId)
|
||||
.arg(currentThinking.length()));
|
||||
m_thinkingStarted.insert(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -102,14 +102,19 @@ 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";
|
||||
const char CA_ALLOW_NETWORK_ACCESS[] = "QodeAssist.caAllowNetworkAccess";
|
||||
const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
||||
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
||||
const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool";
|
||||
const char CA_ENABLE_TERMINAL_COMMAND_TOOL[] = "QodeAssist.caEnableTerminalCommandTool";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS[] = "QodeAssist.caAllowedTerminalCommands";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_LINUX[] = "QodeAssist.caAllowedTerminalCommandsLinux";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS";
|
||||
const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows";
|
||||
|
||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
|
||||
@ -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(
|
||||
@ -61,6 +54,13 @@ ToolsSettings::ToolsSettings()
|
||||
Tr::tr("Allow tools to write and modify files on disk (WARNING: Use with caution!)"));
|
||||
allowFileSystemWrite.setDefaultValue(false);
|
||||
|
||||
allowNetworkAccess.setSettingsKey(Constants::CA_ALLOW_NETWORK_ACCESS);
|
||||
allowNetworkAccess.setLabelText(Tr::tr("Allow Network Access for tools"));
|
||||
allowNetworkAccess.setToolTip(
|
||||
Tr::tr("Allow tools to make network requests (e.g., execute commands like git, curl, wget). "
|
||||
"Required for ExecuteTerminalCommandTool with network-capable commands."));
|
||||
allowNetworkAccess.setDefaultValue(false);
|
||||
|
||||
allowAccessOutsideProject.setSettingsKey(Constants::CA_ALLOW_ACCESS_OUTSIDE_PROJECT);
|
||||
allowAccessOutsideProject.setLabelText(Tr::tr("Allow file access outside project"));
|
||||
allowAccessOutsideProject.setToolTip(
|
||||
@ -97,13 +97,29 @@ ToolsSettings::ToolsSettings()
|
||||
"unexpected behavior."));
|
||||
enableTerminalCommandTool.setDefaultValue(false);
|
||||
|
||||
allowedTerminalCommands.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS);
|
||||
allowedTerminalCommands.setLabelText(Tr::tr("Allowed Terminal Commands"));
|
||||
allowedTerminalCommands.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute. "
|
||||
"Example: git, ls, cat, grep, cmake"));
|
||||
allowedTerminalCommands.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommands.setDefaultValue("git, ls, cat, grep");
|
||||
allowedTerminalCommandsLinux.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_LINUX);
|
||||
allowedTerminalCommandsLinux.setLabelText(Tr::tr("Allowed Commands (Linux)"));
|
||||
allowedTerminalCommandsLinux.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Linux. "
|
||||
"Example: git, ls, cat, grep, find, cmake"));
|
||||
allowedTerminalCommandsLinux.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommandsLinux.setDefaultValue("git, ls, cat, grep, find");
|
||||
|
||||
allowedTerminalCommandsMacOS.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_MACOS);
|
||||
allowedTerminalCommandsMacOS.setLabelText(Tr::tr("Allowed Commands (macOS)"));
|
||||
allowedTerminalCommandsMacOS.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on macOS. "
|
||||
"Example: git, ls, cat, grep, find, cmake"));
|
||||
allowedTerminalCommandsMacOS.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommandsMacOS.setDefaultValue("git, ls, cat, grep, find");
|
||||
|
||||
allowedTerminalCommandsWindows.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS);
|
||||
allowedTerminalCommandsWindows.setLabelText(Tr::tr("Allowed Commands (Windows)"));
|
||||
allowedTerminalCommandsWindows.setToolTip(
|
||||
Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Windows. "
|
||||
"Example: git, dir, type, findstr, where, cmake"));
|
||||
allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
allowedTerminalCommandsWindows.setDefaultValue("git, dir, type, findstr, where");
|
||||
|
||||
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||
|
||||
@ -114,16 +130,25 @@ ToolsSettings::ToolsSettings()
|
||||
setLayouter([this]() {
|
||||
using namespace Layouting;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
auto ¤tOsCommands = allowedTerminalCommandsLinux;
|
||||
#elif defined(Q_OS_MACOS)
|
||||
auto ¤tOsCommands = allowedTerminalCommandsMacOS;
|
||||
#elif defined(Q_OS_WIN)
|
||||
auto ¤tOsCommands = allowedTerminalCommandsWindows;
|
||||
#else
|
||||
auto ¤tOsCommands = allowedTerminalCommandsLinux; // fallback
|
||||
#endif
|
||||
|
||||
return Column{
|
||||
Row{Stretch{1}, resetToDefaults},
|
||||
Space{8},
|
||||
Group{
|
||||
title(Tr::tr("Tool Settings")),
|
||||
Column{
|
||||
useTools,
|
||||
Space{8},
|
||||
allowFileSystemRead,
|
||||
allowFileSystemWrite,
|
||||
allowNetworkAccess,
|
||||
allowAccessOutsideProject
|
||||
}},
|
||||
Space{8},
|
||||
@ -133,7 +158,7 @@ ToolsSettings::ToolsSettings()
|
||||
enableEditFileTool,
|
||||
enableBuildProjectTool,
|
||||
enableTerminalCommandTool,
|
||||
allowedTerminalCommands,
|
||||
currentOsCommands,
|
||||
autoApplyFileEdits}},
|
||||
Stretch{1}};
|
||||
});
|
||||
@ -158,15 +183,17 @@ void ToolsSettings::resetSettingsToDefaults()
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
resetAspect(useTools);
|
||||
resetAspect(allowFileSystemRead);
|
||||
resetAspect(allowFileSystemWrite);
|
||||
resetAspect(allowNetworkAccess);
|
||||
resetAspect(allowAccessOutsideProject);
|
||||
resetAspect(autoApplyFileEdits);
|
||||
resetAspect(enableEditFileTool);
|
||||
resetAspect(enableBuildProjectTool);
|
||||
resetAspect(enableTerminalCommandTool);
|
||||
resetAspect(allowedTerminalCommands);
|
||||
resetAspect(allowedTerminalCommandsLinux);
|
||||
resetAspect(allowedTerminalCommandsMacOS);
|
||||
resetAspect(allowedTerminalCommandsWindows);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,16 +32,18 @@ public:
|
||||
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
Utils::BoolAspect useTools{this};
|
||||
Utils::BoolAspect allowFileSystemRead{this};
|
||||
Utils::BoolAspect allowFileSystemWrite{this};
|
||||
Utils::BoolAspect allowNetworkAccess{this};
|
||||
Utils::BoolAspect allowAccessOutsideProject{this};
|
||||
|
||||
// Experimental features
|
||||
Utils::BoolAspect enableEditFileTool{this};
|
||||
Utils::BoolAspect enableBuildProjectTool{this};
|
||||
Utils::BoolAspect enableTerminalCommandTool{this};
|
||||
Utils::StringAspect allowedTerminalCommands{this};
|
||||
Utils::StringAspect allowedTerminalCommandsLinux{this};
|
||||
Utils::StringAspect allowedTerminalCommandsMacOS{this};
|
||||
Utils::StringAspect allowedTerminalCommandsWindows{this};
|
||||
Utils::BoolAspect autoApplyFileEdits{this};
|
||||
|
||||
private:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
@ -25,13 +25,18 @@
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/buildmanager.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectexplorerconstants.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <projectexplorer/runconfiguration.h>
|
||||
#include <projectexplorer/target.h>
|
||||
#include <projectexplorer/task.h>
|
||||
#include <utils/id.h>
|
||||
#include <QApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaObject>
|
||||
#include <QTimer>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
@ -61,15 +66,17 @@ QString BuildProjectTool::name() const
|
||||
|
||||
QString BuildProjectTool::stringName() const
|
||||
{
|
||||
return "Building project";
|
||||
return "Building and running project";
|
||||
}
|
||||
|
||||
QString BuildProjectTool::description() const
|
||||
{
|
||||
return "Build the current project in Qt Creator and wait for completion. "
|
||||
"Optionally run the project after successful build. "
|
||||
"Returns build status (success/failure) and any compilation errors/warnings after "
|
||||
"the build finishes. "
|
||||
"Optional 'rebuild' parameter: set to true to force a clean rebuild (default: false). "
|
||||
"Optional 'run_after_build' parameter: set to true to run the project after successful build (default: false). "
|
||||
"Note: This operation may take some time depending on project size.";
|
||||
}
|
||||
|
||||
@ -82,6 +89,9 @@ QJsonObject BuildProjectTool::getDefinition(LLMCore::ToolSchemaFormat format) co
|
||||
properties["rebuild"] = QJsonObject{
|
||||
{"type", "boolean"},
|
||||
{"description", "Force a clean rebuild instead of incremental build (default: false)"}};
|
||||
properties["run_after_build"] = QJsonObject{
|
||||
{"type", "boolean"},
|
||||
{"description", "Run the project after successful build (default: false)"}};
|
||||
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray();
|
||||
@ -102,38 +112,36 @@ QJsonObject BuildProjectTool::getDefinition(LLMCore::ToolSchemaFormat format) co
|
||||
|
||||
LLMCore::ToolPermissions BuildProjectTool::requiredPermissions() const
|
||||
{
|
||||
return LLMCore::ToolPermission::None;
|
||||
return LLMCore::ToolPermission::FileSystemRead
|
||||
| LLMCore::ToolPermission::FileSystemWrite;
|
||||
}
|
||||
|
||||
QFuture<QString> BuildProjectTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (!project) {
|
||||
LOG_MESSAGE("BuildProjectTool: No active project found");
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: No active project found. Please open a project in Qt Creator."));
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Active project is '%1'").arg(project->displayName()));
|
||||
|
||||
if (ProjectExplorer::BuildManager::isBuilding(project)) {
|
||||
LOG_MESSAGE("BuildProjectTool: Build is already in progress");
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Build is already in progress. Please wait for it to complete."));
|
||||
}
|
||||
|
||||
if (m_activeBuilds.contains(project)) {
|
||||
LOG_MESSAGE("BuildProjectTool: Build already tracked for this project");
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Build is already being tracked for project '%1'.")
|
||||
.arg(project->displayName()));
|
||||
}
|
||||
|
||||
bool rebuild = input.value("rebuild").toBool(false);
|
||||
bool runAfterBuild = input.value("run_after_build").toBool(false);
|
||||
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Starting %1 for project '%2'")
|
||||
.arg(rebuild ? QString("rebuild") : QString("build"))
|
||||
.arg(project->displayName()));
|
||||
LOG_MESSAGE(QString("BuildProjectTool: %1 project '%2'%3")
|
||||
.arg(rebuild ? QString("Rebuilding") : QString("Building"))
|
||||
.arg(project->displayName())
|
||||
.arg(runAfterBuild ? QString(" (run after build)") : QString()));
|
||||
|
||||
auto promise = QSharedPointer<QPromise<QString>>::create();
|
||||
promise->start();
|
||||
@ -143,6 +151,7 @@ QFuture<QString> BuildProjectTool::executeAsync(const QJsonObject &input)
|
||||
buildInfo.project = project;
|
||||
buildInfo.projectName = project->displayName();
|
||||
buildInfo.isRebuild = rebuild;
|
||||
buildInfo.runAfterBuild = runAfterBuild;
|
||||
|
||||
auto *buildManager = ProjectExplorer::BuildManager::instance();
|
||||
buildInfo.buildFinishedConnection = QObject::connect(
|
||||
@ -166,16 +175,11 @@ QFuture<QString> BuildProjectTool::executeAsync(const QJsonObject &input)
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Build queued, waiting for completion..."));
|
||||
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
void BuildProjectTool::onBuildQueueFinished(bool success)
|
||||
{
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Build queue finished with status: %1")
|
||||
.arg(success ? "SUCCESS" : "FAILURE"));
|
||||
|
||||
QList<ProjectExplorer::Project *> projectsToCleanup;
|
||||
|
||||
for (auto it = m_activeBuilds.begin(); it != m_activeBuilds.end(); ++it) {
|
||||
@ -184,19 +188,21 @@ void BuildProjectTool::onBuildQueueFinished(bool success)
|
||||
if (!ProjectExplorer::BuildManager::isBuilding(project)) {
|
||||
BuildInfo &info = it.value();
|
||||
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Build completed for project '%1'")
|
||||
.arg(info.projectName));
|
||||
|
||||
if (info.promise && info.promise->future().isCanceled()) {
|
||||
LOG_MESSAGE(
|
||||
QString("BuildProjectTool: Promise was cancelled for project '%1', cleaning up")
|
||||
.arg(info.projectName));
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Build cancelled for project '%1'")
|
||||
.arg(info.projectName));
|
||||
projectsToCleanup.append(project);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString result = collectBuildResults(success, info.projectName, info.isRebuild);
|
||||
|
||||
if (success && info.runAfterBuild) {
|
||||
scheduleProjectRun(project, info.projectName, result);
|
||||
} else if (!success && info.runAfterBuild) {
|
||||
result += QString("\n\nProject was not started due to build failure.");
|
||||
}
|
||||
|
||||
if (info.promise) {
|
||||
info.promise->addResult(result);
|
||||
info.promise->finish();
|
||||
@ -211,6 +217,29 @@ void BuildProjectTool::onBuildQueueFinished(bool success)
|
||||
}
|
||||
}
|
||||
|
||||
void BuildProjectTool::scheduleProjectRun(ProjectExplorer::Project *project,
|
||||
const QString &projectName,
|
||||
QString &result)
|
||||
{
|
||||
auto *target = project->activeTarget();
|
||||
if (!target) {
|
||||
result += QString("\n\nError: No active target found for the project.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto *runConfig = target->activeRunConfiguration();
|
||||
if (!runConfig) {
|
||||
result += QString("\n\nError: No active run configuration found for the project.");
|
||||
return;
|
||||
}
|
||||
|
||||
QString runConfigName = runConfig->displayName();
|
||||
result += QString("\n\nProject '%1' will be started with run configuration '%2'.")
|
||||
.arg(projectName, runConfigName);
|
||||
|
||||
ProjectExplorer::ProjectExplorerPlugin::runProject(project, Utils::Id(ProjectExplorer::Constants::NORMAL_RUN_MODE));
|
||||
}
|
||||
|
||||
QString BuildProjectTool::collectBuildResults(
|
||||
bool success, const QString &projectName, bool isRebuild)
|
||||
{
|
||||
@ -254,10 +283,9 @@ QString BuildProjectTool::collectBuildResults(
|
||||
warningCount++;
|
||||
break;
|
||||
default:
|
||||
continue; // Skip non-error/warning tasks
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit to first 50 issues to avoid overwhelming the LLM
|
||||
if (issuesList.size() < 50) {
|
||||
QString issueText = QString("[%1] %2").arg(typeStr, task.description());
|
||||
|
||||
@ -307,9 +335,6 @@ void BuildProjectTool::cleanupBuildInfo(ProjectExplorer::Project *project)
|
||||
if (info.buildFinishedConnection) {
|
||||
disconnect(info.buildFinishedConnection);
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Cleaned up build info for project '%1'")
|
||||
.arg(info.projectName));
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
@ -38,6 +38,7 @@ struct BuildInfo
|
||||
QPointer<ProjectExplorer::Project> project;
|
||||
QString projectName;
|
||||
bool isRebuild = false;
|
||||
bool runAfterBuild = false;
|
||||
QMetaObject::Connection buildFinishedConnection;
|
||||
};
|
||||
|
||||
@ -60,6 +61,9 @@ private slots:
|
||||
void onBuildQueueFinished(bool success);
|
||||
|
||||
private:
|
||||
void scheduleProjectRun(ProjectExplorer::Project *project,
|
||||
const QString &projectName,
|
||||
QString &result);
|
||||
QString collectBuildResults(bool success, const QString &projectName, bool isRebuild);
|
||||
void cleanupBuildInfo(ProjectExplorer::Project *project);
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
@ -36,7 +37,6 @@ namespace QodeAssist::Tools {
|
||||
|
||||
EditFileTool::EditFileTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
, m_ignoreManager(new Context::IgnoreManager(this))
|
||||
{}
|
||||
|
||||
QString EditFileTool::name() const
|
||||
@ -52,11 +52,12 @@ QString EditFileTool::stringName() const
|
||||
QString EditFileTool::description() const
|
||||
{
|
||||
return "Edit a file by replacing old content with new content. "
|
||||
"Provide the filename (or absolute path), old_content to find and replace, "
|
||||
"Provide the file path (absolute or relative to project root), old_content to find and replace, "
|
||||
"and new_content to replace it with. Changes are applied immediately if auto-apply "
|
||||
"is enabled in settings. The user can undo or reapply changes at any time. "
|
||||
"\n\nIMPORTANT:"
|
||||
"\n- ALWAYS read the current file content before editing to ensure accuracy."
|
||||
"\n- Path can be absolute (e.g., /path/to/file.cpp) or relative to project root (e.g., src/main.cpp)."
|
||||
"\n- For EMPTY files: use empty old_content (empty string or omit parameter)."
|
||||
"\n- To append at the END of file: use empty old_content."
|
||||
"\n- To insert at the BEGINNING of a file (e.g., copyright header), you MUST provide "
|
||||
@ -77,8 +78,8 @@ QJsonObject EditFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
|
||||
QJsonObject filenameProperty;
|
||||
filenameProperty["type"] = "string";
|
||||
filenameProperty["description"]
|
||||
= "The filename or absolute path of the file to edit. If only filename is provided, "
|
||||
"it will be searched in the project";
|
||||
= "The path of the file to edit. Can be an absolute path (e.g., /path/to/file.cpp) "
|
||||
"or a relative path from the project root (e.g., src/main.cpp)";
|
||||
properties["filename"] = filenameProperty;
|
||||
|
||||
QJsonObject oldContentProperty;
|
||||
@ -139,24 +140,22 @@ QFuture<QString> EditFileTool::executeAsync(const QJsonObject &input)
|
||||
}
|
||||
|
||||
|
||||
QString filePath;
|
||||
QFileInfo fileInfo(filename);
|
||||
QString filePath;
|
||||
|
||||
if (fileInfo.isAbsolute() && fileInfo.exists()) {
|
||||
if (fileInfo.isAbsolute()) {
|
||||
filePath = filename;
|
||||
} else {
|
||||
FileSearchUtils::FileMatch match = FileSearchUtils::findBestMatch(
|
||||
filename, QString(), 10, m_ignoreManager);
|
||||
|
||||
if (match.absolutePath.isEmpty()) {
|
||||
QString projectRoot = Context::ProjectUtils::getProjectRoot();
|
||||
if (projectRoot.isEmpty()) {
|
||||
throw ToolRuntimeError(
|
||||
QString("File '%1' not found in project. "
|
||||
"Please provide a valid filename or absolute path.")
|
||||
QString("Cannot resolve relative path '%1': no project is open. "
|
||||
"Please provide an absolute path or open a project.")
|
||||
.arg(filename));
|
||||
}
|
||||
|
||||
filePath = match.absolutePath;
|
||||
LOG_MESSAGE(QString("EditFileTool: Found file '%1' at '%2'")
|
||||
filePath = QDir(projectRoot).absoluteFilePath(filename);
|
||||
LOG_MESSAGE(QString("EditFileTool: Resolved relative path '%1' to '%2'")
|
||||
.arg(filename, filePath));
|
||||
}
|
||||
|
||||
|
||||
@ -19,9 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FileSearchUtils.hpp"
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
#include <llmcore/BaseTool.hpp>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
@ -39,9 +36,6 @@ public:
|
||||
LLMCore::ToolPermissions requiredPermissions() const override;
|
||||
|
||||
QFuture<QString> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
private:
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
#include <QPromise>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QTimer>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
@ -50,16 +51,7 @@ QString ExecuteTerminalCommandTool::stringName() const
|
||||
|
||||
QString ExecuteTerminalCommandTool::description() const
|
||||
{
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
|
||||
return QString(
|
||||
"Execute a terminal command in the project directory. "
|
||||
"Only commands from the allowed list can be executed. "
|
||||
"Currently allowed commands: %1. "
|
||||
"The command will be executed in the root directory of the active project. "
|
||||
"Returns the command output (stdout and stderr) or an error message if the command fails.")
|
||||
.arg(allowedList);
|
||||
return getCommandDescription();
|
||||
}
|
||||
|
||||
QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat format) const
|
||||
@ -67,19 +59,18 @@ QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
const QString commandDesc = getCommandDescription();
|
||||
|
||||
QJsonObject properties;
|
||||
properties["command"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
QString("The terminal command to execute. Must be one of the allowed commands: %1")
|
||||
.arg(allowedList)}};
|
||||
{"description", commandDesc}};
|
||||
|
||||
properties["args"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description", "Optional arguments for the command (default: empty)"}};
|
||||
{"description",
|
||||
"Optional arguments for the command. Arguments with spaces should be properly quoted. "
|
||||
"Example: '--file \"path with spaces.txt\" --verbose'"}};
|
||||
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{"command"};
|
||||
@ -100,7 +91,9 @@ QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat
|
||||
|
||||
LLMCore::ToolPermissions ExecuteTerminalCommandTool::requiredPermissions() const
|
||||
{
|
||||
return LLMCore::ToolPermission::None;
|
||||
return LLMCore::ToolPermission::FileSystemRead
|
||||
| LLMCore::ToolPermission::FileSystemWrite
|
||||
| LLMCore::ToolPermission::NetworkAccess;
|
||||
}
|
||||
|
||||
QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &input)
|
||||
@ -113,6 +106,22 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
return QtFuture::makeReadyFuture(QString("Error: Command parameter is required."));
|
||||
}
|
||||
|
||||
if (command.length() > MAX_COMMAND_LENGTH) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command too long (%1 chars)")
|
||||
.arg(command.length()));
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Command exceeds maximum length of %1 characters.")
|
||||
.arg(MAX_COMMAND_LENGTH));
|
||||
}
|
||||
|
||||
if (args.length() > MAX_ARGS_LENGTH) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Arguments too long (%1 chars)")
|
||||
.arg(args.length()));
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Arguments exceed maximum length of %1 characters.")
|
||||
.arg(MAX_ARGS_LENGTH));
|
||||
}
|
||||
|
||||
if (!isCommandAllowed(command)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' is not allowed")
|
||||
.arg(command));
|
||||
@ -124,6 +133,30 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
.arg(allowedList));
|
||||
}
|
||||
|
||||
if (!isCommandSafe(command)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' contains unsafe characters")
|
||||
.arg(command));
|
||||
#ifdef Q_OS_WIN
|
||||
const QString allowedChars = "alphanumeric characters, hyphens, underscores, dots, colons, "
|
||||
"backslashes, and forward slashes";
|
||||
#else
|
||||
const QString allowedChars = "alphanumeric characters, hyphens, underscores, dots, and slashes";
|
||||
#endif
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Command '%1' contains potentially dangerous characters. "
|
||||
"Only %2 are allowed.")
|
||||
.arg(command)
|
||||
.arg(allowedChars));
|
||||
}
|
||||
|
||||
if (!args.isEmpty() && !areArgumentsSafe(args)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Arguments contain unsafe patterns: '%1'")
|
||||
.arg(args));
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Arguments contain potentially dangerous patterns (command chaining, "
|
||||
"redirection, or pipe operators)."));
|
||||
}
|
||||
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
QString workingDir;
|
||||
|
||||
@ -136,9 +169,20 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
workingDir = QDir::currentPath();
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing command '%1' with args '%2'")
|
||||
QDir dir(workingDir);
|
||||
if (!dir.exists() || !dir.isReadable()) {
|
||||
LOG_MESSAGE(
|
||||
QString("ExecuteTerminalCommandTool: Working directory '%1' is not accessible")
|
||||
.arg(workingDir));
|
||||
return QtFuture::makeReadyFuture(
|
||||
QString("Error: Working directory '%1' does not exist or is not accessible.")
|
||||
.arg(workingDir));
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing command '%1' with args '%2' in '%3'")
|
||||
.arg(command)
|
||||
.arg(args));
|
||||
.arg(args.isEmpty() ? "(no args)" : args)
|
||||
.arg(workingDir));
|
||||
|
||||
auto promise = QSharedPointer<QPromise<QString>>::create();
|
||||
QFuture<QString> future = promise->future();
|
||||
@ -147,40 +191,85 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
QProcess *process = new QProcess();
|
||||
process->setWorkingDirectory(workingDir);
|
||||
process->setProcessChannelMode(QProcess::MergedChannels);
|
||||
|
||||
process->setReadChannel(QProcess::StandardOutput);
|
||||
|
||||
QTimer *timeoutTimer = new QTimer();
|
||||
timeoutTimer->setSingleShot(true);
|
||||
timeoutTimer->setInterval(COMMAND_TIMEOUT_MS);
|
||||
|
||||
auto outputSize = QSharedPointer<qint64>::create(0);
|
||||
|
||||
QObject::connect(timeoutTimer, &QTimer::timeout, [process, promise, command, args, timeoutTimer]() {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1 %2' timed out after %3ms")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(COMMAND_TIMEOUT_MS));
|
||||
|
||||
process->terminate();
|
||||
|
||||
QTimer::singleShot(1000, process, [process]() {
|
||||
if (process->state() == QProcess::Running) {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Forcefully killing process after timeout");
|
||||
process->kill();
|
||||
}
|
||||
});
|
||||
|
||||
promise->addResult(QString("Error: Command '%1 %2' timed out after %3 seconds. "
|
||||
"The process has been terminated.")
|
||||
.arg(command)
|
||||
.arg(args.isEmpty() ? "" : args)
|
||||
.arg(COMMAND_TIMEOUT_MS / 1000));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
timeoutTimer->deleteLater();
|
||||
});
|
||||
|
||||
QObject::connect(
|
||||
process,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
[process, promise, command, args](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
const QString output = QString::fromUtf8(process->readAll());
|
||||
[this, process, promise, command, args, timeoutTimer, outputSize](
|
||||
int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
timeoutTimer->stop();
|
||||
timeoutTimer->deleteLater();
|
||||
|
||||
const QByteArray rawOutput = process->readAll();
|
||||
*outputSize += rawOutput.size();
|
||||
const QString output = sanitizeOutput(QString::fromUtf8(rawOutput), *outputSize);
|
||||
|
||||
const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args);
|
||||
|
||||
if (exitStatus == QProcess::NormalExit) {
|
||||
if (exitCode == 0) {
|
||||
LOG_MESSAGE(
|
||||
QString("ExecuteTerminalCommandTool: Command completed successfully"));
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' completed "
|
||||
"successfully (output size: %2 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(*outputSize));
|
||||
promise->addResult(
|
||||
QString("Command '%1 %2' executed successfully.\n\nOutput:\n%3")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
QString("Command '%1' executed successfully.\n\nOutput:\n%2")
|
||||
.arg(fullCommand)
|
||||
.arg(output.isEmpty() ? "(no output)" : output));
|
||||
} else {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command failed with exit "
|
||||
"code %1")
|
||||
.arg(exitCode));
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' failed with "
|
||||
"exit code %2 (output size: %3 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(exitCode)
|
||||
.arg(*outputSize));
|
||||
promise->addResult(
|
||||
QString("Command '%1 %2' failed with exit code %3.\n\nOutput:\n%4")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
QString("Command '%1' failed with exit code %2.\n\nOutput:\n%3")
|
||||
.arg(fullCommand)
|
||||
.arg(exitCode)
|
||||
.arg(output.isEmpty() ? "(no output)" : output));
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Command crashed or was terminated");
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' crashed or was "
|
||||
"terminated (output size: %2 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(*outputSize));
|
||||
const QString error = process->errorString();
|
||||
promise->addResult(
|
||||
QString("Command '%1 %2' crashed or was terminated.\n\nError: %3\n\nOutput:\n%4")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
QString("Command '%1' crashed or was terminated.\n\nError: %2\n\nOutput:\n%3")
|
||||
.arg(fullCommand)
|
||||
.arg(error)
|
||||
.arg(output.isEmpty() ? "(no output)" : output));
|
||||
}
|
||||
@ -189,25 +278,111 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
QObject::connect(process, &QProcess::errorOccurred, [process, promise, command, args](
|
||||
QObject::connect(process, &QProcess::errorOccurred, [process, promise, command, args, timeoutTimer](
|
||||
QProcess::ProcessError error) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process error occurred: %1").arg(error));
|
||||
const QString errorString = process->errorString();
|
||||
promise->addResult(QString("Error executing command '%1 %2': %3")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(errorString));
|
||||
if (promise->future().isFinished()) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutTimer->stop();
|
||||
timeoutTimer->deleteLater();
|
||||
|
||||
const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args);
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process error occurred for '%1': %2 (%3)")
|
||||
.arg(fullCommand)
|
||||
.arg(error)
|
||||
.arg(process->errorString()));
|
||||
|
||||
QString errorMessage;
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
errorMessage = QString("Failed to start command '%1'. The command may not exist or "
|
||||
"you may not have permission to execute it.")
|
||||
.arg(fullCommand);
|
||||
break;
|
||||
case QProcess::Crashed:
|
||||
errorMessage = QString("Command '%1' crashed during execution.").arg(fullCommand);
|
||||
break;
|
||||
case QProcess::Timedout:
|
||||
errorMessage = QString("Command '%1' timed out.").arg(fullCommand);
|
||||
break;
|
||||
case QProcess::WriteError:
|
||||
errorMessage = QString("Write error occurred while executing '%1'.").arg(fullCommand);
|
||||
break;
|
||||
case QProcess::ReadError:
|
||||
errorMessage = QString("Read error occurred while executing '%1'.").arg(fullCommand);
|
||||
break;
|
||||
default:
|
||||
errorMessage = QString("Unknown error occurred while executing '%1': %2")
|
||||
.arg(fullCommand)
|
||||
.arg(process->errorString());
|
||||
break;
|
||||
}
|
||||
|
||||
promise->addResult(QString("Error: %1").arg(errorMessage));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
if (args.isEmpty()) {
|
||||
process->start(command, QStringList());
|
||||
} else {
|
||||
QStringList argList = args.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
process->start(command, argList);
|
||||
QString fullCommand = command;
|
||||
if (!args.isEmpty()) {
|
||||
fullCommand += " " + args;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
static const QStringList windowsBuiltinCommands = {
|
||||
"dir", "type", "del", "copy", "move", "ren", "rename",
|
||||
"md", "mkdir", "rd", "rmdir", "cd", "chdir", "cls", "echo",
|
||||
"set", "path", "prompt", "ver", "vol", "date", "time"
|
||||
};
|
||||
|
||||
const QString lowerCommand = command.toLower();
|
||||
const bool isBuiltin = windowsBuiltinCommands.contains(lowerCommand);
|
||||
|
||||
if (isBuiltin) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing Windows builtin command '%1' via cmd.exe")
|
||||
.arg(command));
|
||||
process->start("cmd.exe", QStringList() << "/c" << fullCommand);
|
||||
} else {
|
||||
#endif
|
||||
QStringList splitCommand = QProcess::splitCommand(fullCommand);
|
||||
if (splitCommand.isEmpty()) {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Failed to parse command");
|
||||
promise->addResult(QString("Error: Failed to parse command '%1'").arg(fullCommand));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
timeoutTimer->deleteLater();
|
||||
return future;
|
||||
}
|
||||
|
||||
const QString program = splitCommand.takeFirst();
|
||||
process->start(program, splitCommand);
|
||||
#ifdef Q_OS_WIN
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!process->waitForStarted(PROCESS_START_TIMEOUT_MS)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Failed to start command '%1' within %2ms")
|
||||
.arg(fullCommand)
|
||||
.arg(PROCESS_START_TIMEOUT_MS));
|
||||
const QString errorString = process->errorString();
|
||||
promise->addResult(QString("Error: Failed to start command '%1': %2\n\n"
|
||||
"Possible reasons:\n"
|
||||
"- Command not found in PATH\n"
|
||||
"- Insufficient permissions\n"
|
||||
"- Invalid command syntax")
|
||||
.arg(fullCommand)
|
||||
.arg(errorString));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
timeoutTimer->deleteLater();
|
||||
return future;
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process started successfully (PID: %1)")
|
||||
.arg(process->processId()));
|
||||
|
||||
timeoutTimer->start();
|
||||
return future;
|
||||
}
|
||||
|
||||
@ -217,24 +392,134 @@ bool ExecuteTerminalCommandTool::isCommandAllowed(const QString &command) const
|
||||
return allowed.contains(command, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool ExecuteTerminalCommandTool::isCommandSafe(const QString &command) const
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
static const QRegularExpression safePattern("^[a-zA-Z0-9._/\\\\:-]+$");
|
||||
#else
|
||||
static const QRegularExpression safePattern("^[a-zA-Z0-9._/-]+$");
|
||||
#endif
|
||||
|
||||
const bool isSafe = safePattern.match(command).hasMatch();
|
||||
if (!isSafe) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' failed safety check")
|
||||
.arg(command));
|
||||
}
|
||||
return isSafe;
|
||||
}
|
||||
|
||||
bool ExecuteTerminalCommandTool::areArgumentsSafe(const QString &args) const
|
||||
{
|
||||
if (args.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static const QStringList dangerousPatterns = {
|
||||
";", // Command separator
|
||||
"&&", // AND operator
|
||||
"||", // OR operator
|
||||
"|", // Pipe operator
|
||||
">", // Output redirection
|
||||
">>", // Append redirection
|
||||
"<", // Input redirection
|
||||
"`", // Command substitution
|
||||
"$(", // Command substitution
|
||||
"$()", // Command substitution
|
||||
"\\n", // Newline (could start new command)
|
||||
"\\r" // Carriage return
|
||||
};
|
||||
|
||||
for (const QString &pattern : dangerousPatterns) {
|
||||
if (args.contains(pattern)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Dangerous pattern '%1' found in args")
|
||||
.arg(pattern));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::sanitizeOutput(const QString &output, qint64 totalSize) const
|
||||
{
|
||||
if (totalSize > MAX_OUTPUT_SIZE) {
|
||||
const QString truncated = output.left(MAX_OUTPUT_SIZE / 2);
|
||||
return QString("%1\n\n... [Output truncated: exceeded maximum size of %2 MB. "
|
||||
"Total output size was %3 bytes] ...")
|
||||
.arg(truncated)
|
||||
.arg(MAX_OUTPUT_SIZE / (1024 * 1024))
|
||||
.arg(totalSize);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
|
||||
{
|
||||
const QString commandsStr
|
||||
= Settings::toolsSettings().allowedTerminalCommands().trimmed();
|
||||
static QString cachedCommandsStr;
|
||||
static QStringList cachedCommands;
|
||||
|
||||
QString commandsStr;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed();
|
||||
#elif defined(Q_OS_MACOS)
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsMacOS().trimmed();
|
||||
#elif defined(Q_OS_WIN)
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsWindows().trimmed();
|
||||
#else
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed(); // fallback
|
||||
#endif
|
||||
|
||||
if (commandsStr == cachedCommandsStr && !cachedCommands.isEmpty()) {
|
||||
return cachedCommands;
|
||||
}
|
||||
|
||||
cachedCommandsStr = commandsStr;
|
||||
cachedCommands.clear();
|
||||
|
||||
if (commandsStr.isEmpty()) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
const QStringList rawCommands = commandsStr.split(',', Qt::SkipEmptyParts);
|
||||
QStringList commands;
|
||||
commands.reserve(rawCommands.size());
|
||||
cachedCommands.reserve(rawCommands.size());
|
||||
|
||||
for (const QString &cmd : rawCommands) {
|
||||
commands.append(cmd.trimmed());
|
||||
const QString trimmed = cmd.trimmed();
|
||||
if (!trimmed.isEmpty()) {
|
||||
cachedCommands.append(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
return cachedCommands;
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::getCommandDescription() const
|
||||
{
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
const QString osInfo = " Running on Linux.";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QString osInfo = " Running on macOS.";
|
||||
#elif defined(Q_OS_WIN)
|
||||
const QString osInfo = " Running on Windows.";
|
||||
#else
|
||||
const QString osInfo = "";
|
||||
#endif
|
||||
|
||||
return QString(
|
||||
"Execute a terminal command in the project directory. "
|
||||
"Only commands from the allowed list can be executed. "
|
||||
"Currently allowed commands for this OS: %1. "
|
||||
"The command will be executed in the root directory of the active project. "
|
||||
"Commands have a %2 second timeout. "
|
||||
"Returns the command output (stdout and stderr) or an error message if the command fails.%3")
|
||||
.arg(allowedList)
|
||||
.arg(COMMAND_TIMEOUT_MS / 1000)
|
||||
.arg(osInfo);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
@ -40,7 +40,18 @@ public:
|
||||
|
||||
private:
|
||||
bool isCommandAllowed(const QString &command) const;
|
||||
bool isCommandSafe(const QString &command) const;
|
||||
bool areArgumentsSafe(const QString &args) const;
|
||||
QStringList getAllowedCommands() const;
|
||||
QString getCommandDescription() const;
|
||||
QString sanitizeOutput(const QString &output, qint64 maxSize) const;
|
||||
|
||||
// Constants for production safety
|
||||
static constexpr int COMMAND_TIMEOUT_MS = 30000; // 30 seconds
|
||||
static constexpr qint64 MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
static constexpr int MAX_COMMAND_LENGTH = 1024;
|
||||
static constexpr int MAX_ARGS_LENGTH = 4096;
|
||||
static constexpr int PROCESS_START_TIMEOUT_MS = 3000;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
@ -158,11 +158,11 @@ QJsonArray ToolsFactory::getToolsDefinitions(
|
||||
}
|
||||
}
|
||||
|
||||
// if (requiredPerms.testFlag(LLMCore::ToolPermission::NetworkAccess)) {
|
||||
// if (!settings.allowNetworkAccess()) {
|
||||
// hasPermission = false;
|
||||
// }
|
||||
// }
|
||||
if (requiredPerms.testFlag(LLMCore::ToolPermission::NetworkAccess)) {
|
||||
if (!settings.allowNetworkAccess()) {
|
||||
hasPermission = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPermission) {
|
||||
toolsArray.append(it.value()->getDefinition(format));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user