mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-02-24 16:02:57 -05:00
feat: Add edit file tool (#249)
* feat: Add edit file tool * feat: Add icons for action buttons
This commit is contained in:
@ -121,9 +121,11 @@ add_qtc_plugin(QodeAssist
|
|||||||
tools/ToolsManager.hpp tools/ToolsManager.cpp
|
tools/ToolsManager.hpp tools/ToolsManager.cpp
|
||||||
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
|
||||||
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
|
||||||
|
tools/EditFileTool.hpp tools/EditFileTool.cpp
|
||||||
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
|
||||||
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||||
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
|
||||||
|
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||||
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
|
||||||
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
|
||||||
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
|
||||||
|
|||||||
@ -16,9 +16,11 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
qml/parts/TopBar.qml
|
qml/parts/TopBar.qml
|
||||||
qml/parts/BottomBar.qml
|
qml/parts/BottomBar.qml
|
||||||
qml/parts/AttachedFilesPlace.qml
|
qml/parts/AttachedFilesPlace.qml
|
||||||
qml/parts/ErrorToast.qml
|
qml/parts/Toast.qml
|
||||||
qml/ToolStatusItem.qml
|
qml/ToolStatusItem.qml
|
||||||
|
qml/FileEditItem.qml
|
||||||
qml/parts/RulesViewer.qml
|
qml/parts/RulesViewer.qml
|
||||||
|
qml/parts/FileEditsActionBar.qml
|
||||||
|
|
||||||
RESOURCES
|
RESOURCES
|
||||||
icons/attach-file-light.svg
|
icons/attach-file-light.svg
|
||||||
@ -36,6 +38,10 @@ qt_add_qml_module(QodeAssistChatView
|
|||||||
icons/chat-icon.svg
|
icons/chat-icon.svg
|
||||||
icons/chat-pause-icon.svg
|
icons/chat-pause-icon.svg
|
||||||
icons/rules-icon.svg
|
icons/rules-icon.svg
|
||||||
|
icons/open-in-editor.svg
|
||||||
|
icons/apply-changes-button.svg
|
||||||
|
icons/undo-changes-button.svg
|
||||||
|
icons/reject-changes-button.svg
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@ -39,6 +40,21 @@ ChatModel::ChatModel(QObject *parent)
|
|||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
this,
|
this,
|
||||||
&ChatModel::tokensThresholdChanged);
|
&ChatModel::tokensThresholdChanged);
|
||||||
|
|
||||||
|
connect(&Context::ChangesManager::instance(),
|
||||||
|
&Context::ChangesManager::fileEditApplied,
|
||||||
|
this,
|
||||||
|
&ChatModel::onFileEditApplied);
|
||||||
|
|
||||||
|
connect(&Context::ChangesManager::instance(),
|
||||||
|
&Context::ChangesManager::fileEditRejected,
|
||||||
|
this,
|
||||||
|
&ChatModel::onFileEditRejected);
|
||||||
|
|
||||||
|
connect(&Context::ChangesManager::instance(),
|
||||||
|
&Context::ChangesManager::fileEditArchived,
|
||||||
|
this,
|
||||||
|
&ChatModel::onFileEditArchived);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ChatModel::rowCount(const QModelIndex &parent) const
|
int ChatModel::rowCount(const QModelIndex &parent) const
|
||||||
@ -106,6 +122,45 @@ void ChatModel::addMessage(
|
|||||||
newMessage.attachments = attachments;
|
newMessage.attachments = attachments;
|
||||||
m_messages.append(newMessage);
|
m_messages.append(newMessage);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
|
|
||||||
|
if (m_loadingFromHistory && role == ChatRole::FileEdit) {
|
||||||
|
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||||
|
if (content.contains(marker)) {
|
||||||
|
int markerPos = content.indexOf(marker);
|
||||||
|
int jsonStart = markerPos + marker.length();
|
||||||
|
|
||||||
|
if (jsonStart < content.length()) {
|
||||||
|
QString jsonStr = content.mid(jsonStart);
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||||
|
|
||||||
|
if (doc.isObject()) {
|
||||||
|
QJsonObject editData = doc.object();
|
||||||
|
QString editId = editData.value("edit_id").toString();
|
||||||
|
QString filePath = editData.value("file").toString();
|
||||||
|
QString oldContent = editData.value("old_content").toString();
|
||||||
|
QString newContent = editData.value("new_content").toString();
|
||||||
|
QString originalStatus = editData.value("status").toString();
|
||||||
|
|
||||||
|
if (!editId.isEmpty() && !filePath.isEmpty()) {
|
||||||
|
Context::ChangesManager::instance().addFileEdit(
|
||||||
|
editId, filePath, oldContent, newContent, false, true);
|
||||||
|
|
||||||
|
editData["status"] = "archived";
|
||||||
|
editData["status_message"] = "Loaded from chat history";
|
||||||
|
|
||||||
|
QString updatedContent = marker
|
||||||
|
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
|
||||||
|
m_messages.last().content = updatedContent;
|
||||||
|
|
||||||
|
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Registered historical file edit: %1 (original status: %2, now: archived)")
|
||||||
|
.arg(editId, originalStatus));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +253,6 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
|
|||||||
break;
|
break;
|
||||||
case ChatRole::Tool:
|
case ChatRole::Tool:
|
||||||
case ChatRole::FileEdit:
|
case ChatRole::FileEdit:
|
||||||
// Skip Tool and FileEdit messages - they are UI-only
|
|
||||||
continue;
|
continue;
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
@ -326,8 +380,11 @@ void ChatModel::updateToolResult(
|
|||||||
} else {
|
} else {
|
||||||
QJsonObject editData = doc.object();
|
QJsonObject editData = doc.object();
|
||||||
|
|
||||||
// Generate unique edit ID based on timestamp
|
QString editId = editData.value("edit_id").toString();
|
||||||
QString editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch());
|
|
||||||
|
if (editId.isEmpty()) {
|
||||||
|
editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch());
|
||||||
|
}
|
||||||
|
|
||||||
LOG_MESSAGE(QString("Adding FileEdit message, editId=%1").arg(editId));
|
LOG_MESSAGE(QString("Adding FileEdit message, editId=%1").arg(editId));
|
||||||
|
|
||||||
@ -345,4 +402,81 @@ void ChatModel::updateToolResult(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_messages.size(); ++i) {
|
||||||
|
if (m_messages[i].id == messageId) {
|
||||||
|
m_messages[i].content = newContent;
|
||||||
|
emit dataChanged(index(i), index(i));
|
||||||
|
LOG_MESSAGE(QString("Updated message content for id: %1").arg(messageId));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatModel::setLoadingFromHistory(bool loading)
|
||||||
|
{
|
||||||
|
m_loadingFromHistory = loading;
|
||||||
|
LOG_MESSAGE(QString("ChatModel loading from history: %1").arg(loading ? "true" : "false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatModel::isLoadingFromHistory() const
|
||||||
|
{
|
||||||
|
return m_loadingFromHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatModel::onFileEditApplied(const QString &editId)
|
||||||
|
{
|
||||||
|
updateFileEditStatus(editId, "applied", "Successfully applied");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatModel::onFileEditRejected(const QString &editId)
|
||||||
|
{
|
||||||
|
updateFileEditStatus(editId, "rejected", "Rejected by user");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatModel::onFileEditArchived(const QString &editId)
|
||||||
|
{
|
||||||
|
updateFileEditStatus(editId, "archived", "Archived (from previous conversation turn)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatModel::updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage)
|
||||||
|
{
|
||||||
|
const QString marker = "QODEASSIST_FILE_EDIT:";
|
||||||
|
|
||||||
|
for (int i = 0; i < m_messages.size(); ++i) {
|
||||||
|
if (m_messages[i].role == ChatRole::FileEdit && m_messages[i].id == editId) {
|
||||||
|
const QString &content = m_messages[i].content;
|
||||||
|
|
||||||
|
if (content.contains(marker)) {
|
||||||
|
int markerPos = content.indexOf(marker);
|
||||||
|
int jsonStart = markerPos + marker.length();
|
||||||
|
|
||||||
|
if (jsonStart < content.length()) {
|
||||||
|
QString jsonStr = content.mid(jsonStart);
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
||||||
|
|
||||||
|
if (doc.isObject()) {
|
||||||
|
QJsonObject editData = doc.object();
|
||||||
|
|
||||||
|
editData["status"] = status;
|
||||||
|
editData["status_message"] = statusMessage;
|
||||||
|
|
||||||
|
QString updatedContent = marker
|
||||||
|
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
|
||||||
|
|
||||||
|
m_messages[i].content = updatedContent;
|
||||||
|
|
||||||
|
emit dataChanged(index(i), index(i));
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Updated FileEdit message status: editId=%1, status=%2")
|
||||||
|
.arg(editId, status));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@ -83,12 +83,25 @@ public:
|
|||||||
const QString &toolId,
|
const QString &toolId,
|
||||||
const QString &toolName,
|
const QString &toolName,
|
||||||
const QString &result);
|
const QString &result);
|
||||||
|
void updateMessageContent(const QString &messageId, const QString &newContent);
|
||||||
|
|
||||||
|
void setLoadingFromHistory(bool loading);
|
||||||
|
bool isLoadingFromHistory() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void tokensThresholdChanged();
|
void tokensThresholdChanged();
|
||||||
void modelReseted();
|
void modelReseted();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onFileEditApplied(const QString &editId);
|
||||||
|
void onFileEditRejected(const QString &editId);
|
||||||
|
void onFileEditArchived(const QString &editId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
|
||||||
|
|
||||||
QVector<Message> m_messages;
|
QVector<Message> m_messages;
|
||||||
|
bool m_loadingFromHistory = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@ -29,14 +29,17 @@
|
|||||||
#include <projectexplorer/project.h>
|
#include <projectexplorer/project.h>
|
||||||
#include <projectexplorer/projectexplorer.h>
|
#include <projectexplorer/projectexplorer.h>
|
||||||
#include <projectexplorer/projectmanager.h>
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
#include <utils/theme/theme.h>
|
#include <utils/theme/theme.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "ToolsSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProjectSettings.hpp"
|
#include "ProjectSettings.hpp"
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
#include "context/ContextManager.hpp"
|
#include "context/ContextManager.hpp"
|
||||||
#include "context/TokenUtils.hpp"
|
#include "context/TokenUtils.hpp"
|
||||||
#include "llmcore/RulesLoader.hpp"
|
#include "llmcore/RulesLoader.hpp"
|
||||||
@ -78,7 +81,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
&ChatRootView::updateInputTokensCount);
|
&ChatRootView::updateInputTokensCount);
|
||||||
|
|
||||||
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
|
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() {
|
||||||
|
setRecentFilePath(QString{});
|
||||||
|
m_currentMessageRequestId.clear();
|
||||||
|
updateCurrentMessageEditsStats();
|
||||||
|
});
|
||||||
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
|
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||||
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
|
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
|
||||||
connect(
|
connect(
|
||||||
@ -138,6 +145,46 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
m_lastErrorMessage = error;
|
m_lastErrorMessage = error;
|
||||||
emit lastErrorMessageChanged();
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
this,
|
||||||
|
[this](const QString &) { updateCurrentMessageEditsStats(); });
|
||||||
|
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
refreshRules();
|
refreshRules();
|
||||||
@ -152,7 +199,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
|||||||
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
|
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
&Settings::generalSettings().useTools,
|
&Settings::toolsSettings().useTools,
|
||||||
&Utils::BaseAspect::changed,
|
&Utils::BaseAspect::changed,
|
||||||
this,
|
this,
|
||||||
&ChatRootView::toolsSupportEnabledChanged);
|
&ChatRootView::toolsSupportEnabledChanged);
|
||||||
@ -258,7 +305,10 @@ void ChatRootView::loadHistory(const QString &filePath)
|
|||||||
} else {
|
} else {
|
||||||
setRecentFilePath(filePath);
|
setRecentFilePath(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_currentMessageRequestId.clear();
|
||||||
updateInputTokensCount();
|
updateInputTokensCount();
|
||||||
|
updateCurrentMessageEditsStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChatRootView::showSaveDialog()
|
void ChatRootView::showSaveDialog()
|
||||||
@ -731,7 +781,310 @@ void ChatRootView::setIsAgentMode(bool newIsAgentMode)
|
|||||||
|
|
||||||
bool ChatRootView::toolsSupportEnabled() const
|
bool ChatRootView::toolsSupportEnabled() const
|
||||||
{
|
{
|
||||||
return Settings::generalSettings().useTools();
|
return Settings::toolsSettings().useTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::applyFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Applying file edit: %1").arg(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);
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::rejectFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(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);
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::undoFileEdit(const QString &editId)
|
||||||
|
{
|
||||||
|
LOG_MESSAGE(QString("Undoing file edit: %1").arg(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);
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
position = currentContent.indexOf(edit.oldContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position >= 0) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
textEditor->editorWidget()->setTextCursor(cursor);
|
||||||
|
textEditor->editorWidget()->centerCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::updateFileEditStatus(const QString &editId, const QString &status)
|
||||||
|
{
|
||||||
|
auto messages = m_chatModel->getChatHistory();
|
||||||
|
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));
|
||||||
|
m_chatModel->updateMessageContent(editId, updatedContent);
|
||||||
|
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentMessageEditsStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::applyAllFileEditsForCurrentMessage()
|
||||||
|
{
|
||||||
|
if (m_currentMessageRequestId.isEmpty()) {
|
||||||
|
m_lastErrorMessage = QString("No active message with file edits");
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
|
||||||
|
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
|
||||||
|
for (const auto &edit : edits) {
|
||||||
|
if (edit.status == Context::ChangesManager::Applied) {
|
||||||
|
updateFileEditStatus(edit.editId, "applied");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCurrentMessageEditsStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatRootView::undoAllFileEditsForCurrentMessage()
|
||||||
|
{
|
||||||
|
if (m_currentMessageRequestId.isEmpty()) {
|
||||||
|
m_lastErrorMessage = QString("No active message with file edits");
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
emit lastErrorMessageChanged();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
m_currentMessageTotalEdits = 0;
|
||||||
|
m_currentMessageAppliedEdits = 0;
|
||||||
|
m_currentMessagePendingEdits = 0;
|
||||||
|
m_currentMessageRejectedEdits = 0;
|
||||||
|
emit currentMessageEditsStatsChanged();
|
||||||
|
}
|
||||||
|
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:
|
||||||
|
applied++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Pending:
|
||||||
|
pending++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Rejected:
|
||||||
|
rejected++;
|
||||||
|
break;
|
||||||
|
case Context::ChangesManager::Archived:
|
||||||
|
total--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (m_currentMessageTotalEdits != total) {
|
||||||
|
m_currentMessageTotalEdits = total;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_currentMessageAppliedEdits != applied) {
|
||||||
|
m_currentMessageAppliedEdits = applied;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_currentMessagePendingEdits != pending) {
|
||||||
|
m_currentMessagePendingEdits = pending;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (m_currentMessageRejectedEdits != rejected) {
|
||||||
|
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));
|
||||||
|
emit currentMessageEditsStatsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::currentMessageTotalEdits() const
|
||||||
|
{
|
||||||
|
return m_currentMessageTotalEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::currentMessageAppliedEdits() const
|
||||||
|
{
|
||||||
|
return m_currentMessageAppliedEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::currentMessagePendingEdits() const
|
||||||
|
{
|
||||||
|
return m_currentMessagePendingEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatRootView::currentMessageRejectedEdits() const
|
||||||
|
{
|
||||||
|
return m_currentMessageRejectedEdits;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatRootView::lastInfoMessage() const
|
||||||
|
{
|
||||||
|
return m_lastInfoMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@ -45,11 +45,17 @@ class ChatRootView : public QQuickItem
|
|||||||
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
|
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
|
||||||
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
|
||||||
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
|
||||||
|
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
|
||||||
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
|
||||||
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
|
||||||
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
|
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
|
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
|
||||||
|
|
||||||
|
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
|
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
|
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
|
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
@ -114,6 +120,23 @@ public:
|
|||||||
void setIsAgentMode(bool newIsAgentMode);
|
void setIsAgentMode(bool newIsAgentMode);
|
||||||
bool toolsSupportEnabled() const;
|
bool toolsSupportEnabled() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void applyFileEdit(const QString &editId);
|
||||||
|
Q_INVOKABLE void rejectFileEdit(const QString &editId);
|
||||||
|
Q_INVOKABLE void undoFileEdit(const QString &editId);
|
||||||
|
Q_INVOKABLE void openFileEditInEditor(const QString &editId);
|
||||||
|
|
||||||
|
// Mass file edit operations for current message
|
||||||
|
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
|
||||||
|
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
|
||||||
|
Q_INVOKABLE void updateCurrentMessageEditsStats();
|
||||||
|
|
||||||
|
int currentMessageTotalEdits() const;
|
||||||
|
int currentMessageAppliedEdits() const;
|
||||||
|
int currentMessagePendingEdits() const;
|
||||||
|
int currentMessageRejectedEdits() const;
|
||||||
|
|
||||||
|
QString lastInfoMessage() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
void copyToClipboard(const QString &text);
|
void copyToClipboard(const QString &text);
|
||||||
@ -138,13 +161,16 @@ signals:
|
|||||||
void isRequestInProgressChanged();
|
void isRequestInProgressChanged();
|
||||||
|
|
||||||
void lastErrorMessageChanged();
|
void lastErrorMessageChanged();
|
||||||
|
void lastInfoMessageChanged();
|
||||||
void activeRulesChanged();
|
void activeRulesChanged();
|
||||||
void activeRulesCountChanged();
|
void activeRulesCountChanged();
|
||||||
|
|
||||||
void isAgentModeChanged();
|
void isAgentModeChanged();
|
||||||
void toolsSupportEnabledChanged();
|
void toolsSupportEnabledChanged();
|
||||||
|
void currentMessageEditsStatsChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
QString getSuggestedFileName() const;
|
QString getSuggestedFileName() const;
|
||||||
|
|
||||||
@ -163,6 +189,13 @@ private:
|
|||||||
QString m_lastErrorMessage;
|
QString m_lastErrorMessage;
|
||||||
QVariantList m_activeRules;
|
QVariantList m_activeRules;
|
||||||
bool m_isAgentMode;
|
bool m_isAgentMode;
|
||||||
|
|
||||||
|
QString m_currentMessageRequestId;
|
||||||
|
int m_currentMessageTotalEdits{0};
|
||||||
|
int m_currentMessageAppliedEdits{0};
|
||||||
|
int m_currentMessagePendingEdits{0};
|
||||||
|
int m_currentMessageRejectedEdits{0};
|
||||||
|
QString m_lastInfoMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@ -120,9 +120,14 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
|
|||||||
}
|
}
|
||||||
|
|
||||||
model->clear();
|
model->clear();
|
||||||
|
|
||||||
|
model->setLoadingFromHistory(true);
|
||||||
|
|
||||||
for (const auto &message : messages) {
|
for (const auto &message : messages) {
|
||||||
model->addMessage(message.content, message.role, message.id);
|
model->addMessage(message.content, message.role, message.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model->setLoadingFromHistory(false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,9 +37,11 @@
|
|||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "ToolsSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
#include "RequestConfig.hpp"
|
#include "RequestConfig.hpp"
|
||||||
|
#include <context/ChangesManager.h>
|
||||||
#include <RulesLoader.hpp>
|
#include <RulesLoader.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
@ -65,6 +67,8 @@ void ClientInterface::sendMessage(
|
|||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
m_accumulatedResponses.clear();
|
m_accumulatedResponses.clear();
|
||||||
|
|
||||||
|
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||||
|
|
||||||
auto attachFiles = m_contextManager->getContentFiles(attachments);
|
auto attachFiles = m_contextManager->getContentFiles(attachments);
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
||||||
@ -89,7 +93,7 @@ void ClientInterface::sendMessage(
|
|||||||
|
|
||||||
LLMCore::ContextData context;
|
LLMCore::ContextData context;
|
||||||
|
|
||||||
const bool isToolsEnabled = Settings::generalSettings().useTools() && useAgentMode;
|
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode;
|
||||||
|
|
||||||
if (chatAssistantSettings.useSystemPrompt()) {
|
if (chatAssistantSettings.useSystemPrompt()) {
|
||||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
@ -155,6 +159,8 @@ void ClientInterface::sendMessage(
|
|||||||
QJsonObject request{{"id", requestId}};
|
QJsonObject request{{"id", requestId}};
|
||||||
|
|
||||||
m_activeRequests[requestId] = {request, provider};
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
|
||||||
|
emit requestStarted(requestId);
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
provider,
|
provider,
|
||||||
@ -312,6 +318,16 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString
|
|||||||
const RequestContext &ctx = it.value();
|
const RequestContext &ctx = it.value();
|
||||||
|
|
||||||
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
|
||||||
|
|
||||||
|
QString applyError;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
handleLLMResponse(finalText, ctx.originalRequest, true);
|
handleLLMResponse(finalText, ctx.originalRequest, true);
|
||||||
|
|
||||||
m_activeRequests.erase(it);
|
m_activeRequests.erase(it);
|
||||||
|
|||||||
@ -52,6 +52,7 @@ public:
|
|||||||
signals:
|
signals:
|
||||||
void errorOccurred(const QString &error);
|
void errorOccurred(const QString &error);
|
||||||
void messageReceivedCompletely();
|
void messageReceivedCompletely();
|
||||||
|
void requestStarted(const QString &requestId);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
void handlePartialResponse(const QString &requestId, const QString &partialText);
|
||||||
|
|||||||
15
ChatView/icons/apply-changes-button.svg
Normal file
15
ChatView/icons/apply-changes-button.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_74_61)">
|
||||||
|
<mask id="mask0_74_61" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||||
|
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_74_61)">
|
||||||
|
<path d="M8 22L18 32L36 12" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_74_61">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 548 B |
17
ChatView/icons/open-in-editor.svg
Normal file
17
ChatView/icons/open-in-editor.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_74_52)">
|
||||||
|
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||||
|
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_74_52)">
|
||||||
|
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
|
||||||
|
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_74_52">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 943 B |
16
ChatView/icons/reject-changes-button.svg
Normal file
16
ChatView/icons/reject-changes-button.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_74_76)">
|
||||||
|
<mask id="mask0_74_76" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||||
|
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_74_76)">
|
||||||
|
<path d="M12 12L32 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M32 12L12 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_74_76">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 599 B |
16
ChatView/icons/undo-changes-button.svg
Normal file
16
ChatView/icons/undo-changes-button.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_74_68)">
|
||||||
|
<mask id="mask0_74_68" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
|
||||||
|
<path d="M44 0H0V44H44V0Z" fill="white"/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_74_68)">
|
||||||
|
<path d="M12 12L6 18L12 24" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6 18H28C33 18 38 23 38 28C38 33 33 38 28 38H22" stroke="black" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_74_68">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 663 B |
423
ChatView/qml/FileEditItem.qml
Normal file
423
ChatView/qml/FileEditItem.qml
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import UIControls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string editContent: ""
|
||||||
|
|
||||||
|
readonly property var editData: parseEditData(editContent)
|
||||||
|
readonly property string filePath: editData.file || ""
|
||||||
|
readonly property string fileName: getFileName(filePath)
|
||||||
|
readonly property string editStatus: editData.status || "pending"
|
||||||
|
readonly property string statusMessage: editData.status_message || ""
|
||||||
|
readonly property string oldContent: editData.old_content || ""
|
||||||
|
readonly property string newContent: editData.new_content || ""
|
||||||
|
|
||||||
|
signal applyEdit(string editId)
|
||||||
|
signal rejectEdit(string editId)
|
||||||
|
signal undoEdit(string editId)
|
||||||
|
signal openInEditor(string editId)
|
||||||
|
|
||||||
|
readonly property int borderRadius: 4
|
||||||
|
readonly property int contentMargin: 10
|
||||||
|
readonly property int contentBottomPadding: 20
|
||||||
|
readonly property int headerPadding: 8
|
||||||
|
readonly property int statusIndicatorWidth: 4
|
||||||
|
|
||||||
|
readonly property bool isPending: editStatus === "pending"
|
||||||
|
readonly property bool isApplied: editStatus === "applied"
|
||||||
|
readonly property bool isRejected: editStatus === "rejected"
|
||||||
|
readonly property bool isArchived: editStatus === "archived"
|
||||||
|
|
||||||
|
readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
||||||
|
readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
||||||
|
readonly property color rejectedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
|
||||||
|
readonly property color archivedColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
|
||||||
|
readonly property color pendingColor: palette.highlight
|
||||||
|
|
||||||
|
readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
||||||
|
readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3)
|
||||||
|
readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3)
|
||||||
|
readonly property color archivedBgColor: Qt.rgba(0.5, 0.5, 0.5, 0.3)
|
||||||
|
|
||||||
|
readonly property string codeFontFamily: {
|
||||||
|
switch (Qt.platform.os) {
|
||||||
|
case "windows": return "Consolas"
|
||||||
|
case "osx": return "Menlo"
|
||||||
|
case "linux": return "DejaVu Sans Mono"
|
||||||
|
default: return "monospace"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property int codeFontSize: Qt.application.font.pointSize
|
||||||
|
|
||||||
|
readonly property color statusColor: {
|
||||||
|
if (isArchived) return archivedColor
|
||||||
|
if (isApplied) return appliedColor
|
||||||
|
if (isRejected) return rejectedColor
|
||||||
|
return pendingColor
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property color statusBgColor: {
|
||||||
|
if (isArchived) return archivedBgColor
|
||||||
|
if (isApplied) return appliedBgColor
|
||||||
|
if (isRejected) return rejectedBgColor
|
||||||
|
return palette.button
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string statusText: {
|
||||||
|
if (isArchived) return qsTr("ARCHIVED")
|
||||||
|
if (isApplied) return qsTr("APPLIED")
|
||||||
|
if (isRejected) return qsTr("REJECTED")
|
||||||
|
return qsTr("PENDING")
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int addedLines: countLines(newContent)
|
||||||
|
readonly property int removedLines: countLines(oldContent)
|
||||||
|
|
||||||
|
function parseEditData(content) {
|
||||||
|
try {
|
||||||
|
const marker = "QODEASSIST_FILE_EDIT:";
|
||||||
|
let jsonStr = content;
|
||||||
|
if (content.indexOf(marker) >= 0) {
|
||||||
|
jsonStr = content.substring(content.indexOf(marker) + marker.length);
|
||||||
|
}
|
||||||
|
return JSON.parse(jsonStr);
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
edit_id: "",
|
||||||
|
file: "",
|
||||||
|
old_content: "",
|
||||||
|
new_content: "",
|
||||||
|
status: "error",
|
||||||
|
status_message: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName(path) {
|
||||||
|
if (!path) return "";
|
||||||
|
const parts = path.split('/');
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function countLines(text) {
|
||||||
|
if (!text) return 0;
|
||||||
|
return text.split('\n').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: fileEditView.implicitHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: fileEditView
|
||||||
|
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
implicitHeight: expanded ? headerArea.height + contentColumn.implicitHeight + root.contentBottomPadding + root.contentMargin * 2
|
||||||
|
: headerArea.height
|
||||||
|
radius: root.borderRadius
|
||||||
|
|
||||||
|
color: palette.base
|
||||||
|
|
||||||
|
border.width: 1
|
||||||
|
border.color: root.isPending
|
||||||
|
? (color.hslLightness > 0.5 ? Qt.darker(color, 1.3) : Qt.lighter(color, 1.3))
|
||||||
|
: Qt.alpha(root.statusColor, 0.6)
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "expanded"
|
||||||
|
when: fileEditView.expanded
|
||||||
|
PropertyChanges { target: contentColumn; opacity: 1 }
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "collapsed"
|
||||||
|
when: !fileEditView.expanded
|
||||||
|
PropertyChanges { target: contentColumn; opacity: 0 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
transitions: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
properties: "opacity"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: headerArea
|
||||||
|
width: parent.width
|
||||||
|
height: headerRow.height + 16
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: fileEditView.expanded = !fileEditView.expanded
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: headerRow
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
left: parent.left
|
||||||
|
right: actionButtons.left
|
||||||
|
leftMargin: root.contentMargin
|
||||||
|
rightMargin: root.contentMargin
|
||||||
|
}
|
||||||
|
spacing: root.headerPadding
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: root.statusIndicatorWidth
|
||||||
|
height: headerText.height
|
||||||
|
radius: 2
|
||||||
|
color: root.statusColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: headerText
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: {
|
||||||
|
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
|
||||||
|
if (root.oldContent.length > 0) {
|
||||||
|
return qsTr("%1: %2 (+%3 -%4)")
|
||||||
|
.arg(modeText)
|
||||||
|
.arg(root.fileName)
|
||||||
|
.arg(root.addedLines)
|
||||||
|
.arg(root.removedLines)
|
||||||
|
} else {
|
||||||
|
return qsTr("%1: %2 (+%3)")
|
||||||
|
.arg(modeText)
|
||||||
|
.arg(root.fileName)
|
||||||
|
.arg(root.addedLines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: fileEditView.expanded ? "▼" : "▶"
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: !root.isPending
|
||||||
|
Layout.preferredWidth: badgeText.width + 12
|
||||||
|
Layout.preferredHeight: badgeText.height + 4
|
||||||
|
color: root.statusBgColor
|
||||||
|
radius: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: badgeText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.statusText
|
||||||
|
font.pixelSize: 9
|
||||||
|
font.bold: true
|
||||||
|
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: actionButtons
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: 5
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: root.openInEditor(editData.edit_id)
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Open file in editor and navigate to changes")
|
||||||
|
ToolTip.delay: 500
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/apply-changes-button.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
} enabled: (root.isPending || root.isRejected) && !root.isArchived
|
||||||
|
visible: !root.isApplied && !root.isArchived
|
||||||
|
onClicked: root.applyEdit(editData.edit_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
enabled: root.isApplied && !root.isArchived
|
||||||
|
visible: root.isApplied && !root.isArchived
|
||||||
|
onClicked: root.undoEdit(editData.edit_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/reject-changes-button.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
enabled: root.isPending && !root.isArchived
|
||||||
|
visible: root.isPending && !root.isArchived
|
||||||
|
onClicked: root.rejectEdit(editData.edit_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: contentColumn
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
top: headerArea.bottom
|
||||||
|
margins: root.contentMargin
|
||||||
|
}
|
||||||
|
spacing: 8
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.filePath
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: palette.mid
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: oldContentColumn.implicitHeight + 12
|
||||||
|
color: Qt.rgba(1, 0.2, 0.2, 0.1)
|
||||||
|
radius: 4
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(1, 0.2, 0.2, 0.3)
|
||||||
|
visible: root.oldContent.length > 0
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: oldContentColumn
|
||||||
|
width: parent.width
|
||||||
|
x: 6
|
||||||
|
y: 6
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("- Removed:")
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.bold: true
|
||||||
|
color: Qt.rgba(1, 0.2, 0.2, 0.9)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: oldContentText
|
||||||
|
width: parent.width - 12
|
||||||
|
height: contentHeight
|
||||||
|
text: root.oldContent
|
||||||
|
font.family: root.codeFontFamily
|
||||||
|
font.pixelSize: root.codeFontSize
|
||||||
|
color: palette.text
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
selectByKeyboard: true
|
||||||
|
textFormat: TextEdit.PlainText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: newContentColumn.implicitHeight + 12
|
||||||
|
color: Qt.rgba(0.2, 0.8, 0.2, 0.1)
|
||||||
|
radius: 4
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(0.2, 0.8, 0.2, 0.3)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: newContentColumn
|
||||||
|
width: parent.width
|
||||||
|
x: 6
|
||||||
|
y: 6
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("+ Added:")
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.bold: true
|
||||||
|
color: Qt.rgba(0.2, 0.8, 0.2, 0.9)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: newContentText
|
||||||
|
width: parent.width - 12
|
||||||
|
height: contentHeight
|
||||||
|
text: root.newContent
|
||||||
|
font.family: root.codeFontFamily
|
||||||
|
font.pixelSize: root.codeFontSize
|
||||||
|
color: palette.text
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
selectByKeyboard: true
|
||||||
|
textFormat: TextEdit.PlainText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.statusMessage.length > 0
|
||||||
|
text: root.statusMessage
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.italic: true
|
||||||
|
color: root.isApplied
|
||||||
|
? Qt.rgba(0.2, 0.6, 0.2, 1)
|
||||||
|
: Qt.rgba(0.8, 0.2, 0.2, 1)
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -115,7 +115,7 @@ ChatRootView {
|
|||||||
if (model.roleType === ChatModel.Tool) {
|
if (model.roleType === ChatModel.Tool) {
|
||||||
return toolMessageComponent
|
return toolMessageComponent
|
||||||
} else if (model.roleType === ChatModel.FileEdit) {
|
} else if (model.roleType === ChatModel.FileEdit) {
|
||||||
return toolMessageComponent
|
return fileEditMessageComponent
|
||||||
} else {
|
} else {
|
||||||
return chatItemComponent
|
return chatItemComponent
|
||||||
}
|
}
|
||||||
@ -174,6 +174,31 @@ ChatRootView {
|
|||||||
toolContent: model.content
|
toolContent: model.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: fileEditMessageComponent
|
||||||
|
|
||||||
|
FileEditItem {
|
||||||
|
width: parent.width
|
||||||
|
editContent: model.content
|
||||||
|
|
||||||
|
onApplyEdit: function(editId) {
|
||||||
|
root.applyFileEdit(editId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onRejectEdit: function(editId) {
|
||||||
|
root.rejectFileEdit(editId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUndoEdit: function(editId) {
|
||||||
|
root.undoFileEdit(editId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenInEditor: function(editId) {
|
||||||
|
root.openFileEditInEditor(editId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@ -280,6 +305,19 @@ ChatRootView {
|
|||||||
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileEditsActionBar {
|
||||||
|
id: fileEditsActionBar
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
totalEdits: root.currentMessageTotalEdits
|
||||||
|
appliedEdits: root.currentMessageAppliedEdits
|
||||||
|
pendingEdits: root.currentMessagePendingEdits
|
||||||
|
rejectedEdits: root.currentMessageRejectedEdits
|
||||||
|
|
||||||
|
onApplyAllClicked: root.applyAllFileEditsForCurrentMessage()
|
||||||
|
onUndoAllClicked: root.undoAllFileEditsForCurrentMessage()
|
||||||
|
}
|
||||||
|
|
||||||
BottomBar {
|
BottomBar {
|
||||||
id: bottomBar
|
id: bottomBar
|
||||||
|
|
||||||
@ -329,9 +367,22 @@ ChatRootView {
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorToast {
|
Toast {
|
||||||
id: errorToast
|
id: errorToast
|
||||||
z: 1000
|
z: 1000
|
||||||
|
|
||||||
|
color: Qt.rgba(0.8, 0.2, 0.2, 0.7)
|
||||||
|
border.color: Qt.darker(infoToast.color, 1.3)
|
||||||
|
toastTextColor: "#FFFFFF"
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast {
|
||||||
|
id: infoToast
|
||||||
|
z: 1000
|
||||||
|
|
||||||
|
color: Qt.rgba(0.2, 0.8, 0.2, 0.7)
|
||||||
|
border.color: Qt.darker(infoToast.color, 1.3)
|
||||||
|
toastTextColor: "#FFFFFF"
|
||||||
}
|
}
|
||||||
|
|
||||||
RulesViewer {
|
RulesViewer {
|
||||||
@ -356,6 +407,11 @@ ChatRootView {
|
|||||||
errorToast.show(root.lastErrorMessage)
|
errorToast.show(root.lastErrorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function onLastInfoMessageChanged() {
|
||||||
|
if (root.lastInfoMessage.length > 0) {
|
||||||
|
infoToast.show(root.lastInfoMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
|||||||
@ -87,6 +87,7 @@ Rectangle {
|
|||||||
TextEdit {
|
TextEdit {
|
||||||
id: resultText
|
id: resultText
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
text: root.toolResult
|
text: root.toolResult
|
||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
|
|||||||
161
ChatView/qml/parts/FileEditsActionBar.qml
Normal file
161
ChatView/qml/parts/FileEditsActionBar.qml
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import UIControls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property int totalEdits: 0
|
||||||
|
property int appliedEdits: 0
|
||||||
|
property int pendingEdits: 0
|
||||||
|
property int rejectedEdits: 0
|
||||||
|
property bool hasAppliedEdits: appliedEdits > 0
|
||||||
|
property bool hasRejectedEdits: rejectedEdits > 0
|
||||||
|
property bool hasPendingEdits: pendingEdits > 0
|
||||||
|
|
||||||
|
signal applyAllClicked()
|
||||||
|
signal undoAllClicked()
|
||||||
|
|
||||||
|
visible: totalEdits > 0
|
||||||
|
implicitHeight: visible ? 40 : 0
|
||||||
|
|
||||||
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
|
Qt.darker(palette.window, 1.05) :
|
||||||
|
Qt.lighter(palette.window, 1.05)
|
||||||
|
|
||||||
|
border.width: 1
|
||||||
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 10
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: 10
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.preferredWidth: 24
|
||||||
|
Layout.preferredHeight: 24
|
||||||
|
radius: 12
|
||||||
|
color: {
|
||||||
|
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.2)
|
||||||
|
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.2)
|
||||||
|
return Qt.rgba(0.8, 0.6, 0.2, 0.2)
|
||||||
|
}
|
||||||
|
border.width: 2
|
||||||
|
border.color: {
|
||||||
|
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.8)
|
||||||
|
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.8)
|
||||||
|
return Qt.rgba(0.8, 0.6, 0.2, 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.totalEdits
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status text
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.totalEdits === 1
|
||||||
|
? qsTr("File Edit in Current Message")
|
||||||
|
: qsTr("%1 File Edits in Current Message").arg(root.totalEdits)
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: root.totalEdits > 0
|
||||||
|
text: {
|
||||||
|
let parts = [];
|
||||||
|
if (root.appliedEdits > 0) {
|
||||||
|
parts.push(qsTr("%1 applied").arg(root.appliedEdits));
|
||||||
|
}
|
||||||
|
if (root.pendingEdits > 0) {
|
||||||
|
parts.push(qsTr("%1 pending").arg(root.pendingEdits));
|
||||||
|
}
|
||||||
|
if (root.rejectedEdits > 0) {
|
||||||
|
parts.push(qsTr("%1 rejected").arg(root.rejectedEdits));
|
||||||
|
}
|
||||||
|
return parts.join(", ");
|
||||||
|
}
|
||||||
|
font.pixelSize: 9
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: applyAllButton
|
||||||
|
|
||||||
|
visible: root.hasPendingEdits || root.hasRejectedEdits
|
||||||
|
enabled: root.hasPendingEdits || root.hasRejectedEdits
|
||||||
|
text: root.hasPendingEdits
|
||||||
|
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
|
||||||
|
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: root.hasPendingEdits
|
||||||
|
? qsTr("Apply all pending and rejected edits in this message")
|
||||||
|
: qsTr("Reapply all rejected edits in this message")
|
||||||
|
|
||||||
|
onClicked: root.applyAllClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: undoAllButton
|
||||||
|
|
||||||
|
visible: root.hasAppliedEdits
|
||||||
|
enabled: root.hasAppliedEdits
|
||||||
|
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Undo all applied edits in this message")
|
||||||
|
|
||||||
|
onClicked: root.undoAllClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -20,13 +20,16 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: errorToast
|
id: root
|
||||||
|
|
||||||
|
property alias toastTextItem: textItem
|
||||||
|
property alias toastTextColor: textItem.color
|
||||||
|
|
||||||
property string errorText: ""
|
property string errorText: ""
|
||||||
property int displayDuration: 5000
|
property int displayDuration: 5000
|
||||||
|
|
||||||
width: Math.min(parent.width - 40, errorTextItem.implicitWidth + radius)
|
width: Math.min(parent.width - 40, textItem.implicitWidth + radius)
|
||||||
height: visible ? (errorTextItem.implicitHeight + 12) : 0
|
height: visible ? (textItem.implicitHeight + 12) : 0
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: 10
|
anchors.topMargin: 10
|
||||||
@ -39,15 +42,15 @@ Rectangle {
|
|||||||
opacity: 0
|
opacity: 0
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: errorTextItem
|
id: textItem
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
anchors.margins: 6
|
anchors.margins: 6
|
||||||
text: errorToast.errorText
|
text: root.errorText
|
||||||
color: "#ffffff"
|
color: palette.text
|
||||||
font.pixelSize: 13
|
font.pixelSize: 13
|
||||||
wrapMode: TextEdit.Wrap
|
wrapMode: TextEdit.Wrap
|
||||||
width: Math.min(implicitWidth, errorToast.parent.width - 60)
|
width: Math.min(implicitWidth, root.parent.width - 60)
|
||||||
horizontalAlignment: TextEdit.AlignHCenter
|
horizontalAlignment: TextEdit.AlignHCenter
|
||||||
readOnly: true
|
readOnly: true
|
||||||
selectByMouse: true
|
selectByMouse: true
|
||||||
@ -69,7 +72,7 @@ Rectangle {
|
|||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: showAnimation
|
id: showAnimation
|
||||||
|
|
||||||
target: errorToast
|
target: root
|
||||||
property: "opacity"
|
property: "opacity"
|
||||||
from: 0
|
from: 0
|
||||||
to: 1
|
to: 1
|
||||||
@ -80,21 +83,21 @@ Rectangle {
|
|||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: hideAnimation
|
id: hideAnimation
|
||||||
|
|
||||||
target: errorToast
|
target: root
|
||||||
property: "opacity"
|
property: "opacity"
|
||||||
from: 1
|
from: 1
|
||||||
to: 0
|
to: 0
|
||||||
duration: 200
|
duration: 200
|
||||||
easing.type: Easing.InQuad
|
easing.type: Easing.InQuad
|
||||||
onFinished: errorToast.visible = false
|
onFinished: root.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: hideTimer
|
id: hideTimer
|
||||||
|
|
||||||
interval: errorToast.displayDuration
|
interval: root.displayDuration
|
||||||
running: false
|
running: false
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: errorToast.hide()
|
onTriggered: root.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,9 +54,10 @@ Item {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
id: slider
|
id: slider
|
||||||
|
|
||||||
x: root.checked ? parent.width / 2 : 0
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
x: root.checked ? parent.width / 2 - 1 : 1
|
||||||
width: parent.width / 2
|
width: parent.width / 2
|
||||||
height: parent.height
|
height: parent.height - 2
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -22,8 +22,10 @@
|
|||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
#include <QMutex>
|
||||||
#include <QQueue>
|
#include <QQueue>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QUndoStack>
|
||||||
|
|
||||||
namespace QodeAssist::Context {
|
namespace QodeAssist::Context {
|
||||||
|
|
||||||
@ -39,21 +41,117 @@ public:
|
|||||||
QString lineContent;
|
QString lineContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum FileEditStatus { Pending, Applied, Rejected, Archived };
|
||||||
|
|
||||||
|
struct DiffHunk
|
||||||
|
{
|
||||||
|
int oldStartLine; // Starting line in old file (1-based)
|
||||||
|
int oldLineCount; // Number of lines in old file
|
||||||
|
int newStartLine; // Starting line in new file (1-based)
|
||||||
|
int newLineCount; // Number of lines in new file
|
||||||
|
QStringList contextBefore; // Lines of context before the change (for anchoring)
|
||||||
|
QStringList removedLines; // Lines to remove (prefixed with -)
|
||||||
|
QStringList addedLines; // Lines to add (prefixed with +)
|
||||||
|
QStringList contextAfter; // Lines of context after the change (for anchoring)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DiffInfo
|
||||||
|
{
|
||||||
|
QList<DiffHunk> hunks; // List of diff hunks
|
||||||
|
QString originalContent; // Full original file content (for fallback)
|
||||||
|
QString modifiedContent; // Full modified file content (for fallback)
|
||||||
|
int contextLines = 3; // Number of context lines to keep
|
||||||
|
bool useFallback = false; // If true, use original content-based approach
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileEdit
|
||||||
|
{
|
||||||
|
QString editId;
|
||||||
|
QString filePath;
|
||||||
|
QString oldContent; // Kept for backward compatibility and fallback
|
||||||
|
QString newContent; // Kept for backward compatibility and fallback
|
||||||
|
DiffInfo diffInfo; // Initial diff (created once, may become stale after formatting)
|
||||||
|
FileEditStatus status;
|
||||||
|
QDateTime timestamp;
|
||||||
|
bool wasAutoApplied = false; // Track if edit was already auto-applied once
|
||||||
|
bool isFromHistory = false; // Track if edit was loaded from chat history
|
||||||
|
QString statusMessage;
|
||||||
|
};
|
||||||
|
|
||||||
static ChangesManager &instance();
|
static ChangesManager &instance();
|
||||||
|
|
||||||
void addChange(
|
void addChange(
|
||||||
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
|
TextEditor::TextDocument *document, int position, int charsRemoved, int charsAdded);
|
||||||
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
|
QString getRecentChangesContext(const TextEditor::TextDocument *currentDocument) const;
|
||||||
|
|
||||||
|
void addFileEdit(
|
||||||
|
const QString &editId,
|
||||||
|
const QString &filePath,
|
||||||
|
const QString &oldContent,
|
||||||
|
const QString &newContent,
|
||||||
|
bool autoApply = true,
|
||||||
|
bool isFromHistory = false,
|
||||||
|
const QString &requestId = QString());
|
||||||
|
bool applyFileEdit(const QString &editId);
|
||||||
|
bool rejectFileEdit(const QString &editId);
|
||||||
|
bool undoFileEdit(const QString &editId);
|
||||||
|
FileEdit getFileEdit(const QString &editId) const;
|
||||||
|
QList<FileEdit> getPendingEdits() const;
|
||||||
|
|
||||||
|
bool applyPendingEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
||||||
|
|
||||||
|
QList<FileEdit> getEditsForRequest(const QString &requestId) const;
|
||||||
|
|
||||||
|
bool undoAllEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
||||||
|
|
||||||
|
bool reapplyAllEditsForRequest(const QString &requestId, QString *errorMsg = nullptr);
|
||||||
|
|
||||||
|
void archiveAllNonArchivedEdits();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileEditAdded(const QString &editId);
|
||||||
|
void fileEditApplied(const QString &editId);
|
||||||
|
void fileEditRejected(const QString &editId);
|
||||||
|
void fileEditUndone(const QString &editId);
|
||||||
|
void fileEditArchived(const QString &editId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ChangesManager();
|
ChangesManager();
|
||||||
~ChangesManager();
|
~ChangesManager();
|
||||||
ChangesManager(const ChangesManager &) = delete;
|
ChangesManager(const ChangesManager &) = delete;
|
||||||
ChangesManager &operator=(const ChangesManager &) = delete;
|
ChangesManager &operator=(const ChangesManager &) = delete;
|
||||||
|
|
||||||
void cleanupOldChanges();
|
bool performFileEdit(const QString &filePath, const QString &oldContent, const QString &newContent, QString *errorMsg = nullptr);
|
||||||
|
bool performFileEditWithDiff(const QString &filePath, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
|
||||||
|
QString readFileContent(const QString &filePath) const;
|
||||||
|
|
||||||
|
DiffInfo createDiffInfo(const QString &originalContent, const QString &modifiedContent, const QString &filePath);
|
||||||
|
bool applyDiffToContent(QString &content, const DiffInfo &diffInfo, bool reverse, QString *errorMsg = nullptr);
|
||||||
|
bool findHunkLocation(const QStringList &fileLines, const DiffHunk &hunk, int &actualStartLine, QString *debugInfo = nullptr) const;
|
||||||
|
|
||||||
|
// Helper method for fragment-based apply/undo operations
|
||||||
|
bool performFragmentReplacement(
|
||||||
|
const QString &filePath,
|
||||||
|
const QString &searchContent,
|
||||||
|
const QString &replaceContent,
|
||||||
|
bool isAppendOperation,
|
||||||
|
QString *errorMsg = nullptr);
|
||||||
|
|
||||||
|
int levenshteinDistance(const QString &s1, const QString &s2) const;
|
||||||
|
QString findBestMatch(const QString &fileContent, const QString &searchContent, double threshold = 0.8, double *outSimilarity = nullptr) const;
|
||||||
|
QString findBestMatchWithNormalization(const QString &fileContent, const QString &searchContent, double *outSimilarity = nullptr, QString *outMatchType = nullptr) const;
|
||||||
|
|
||||||
|
struct RequestEdits
|
||||||
|
{
|
||||||
|
QStringList editIds;
|
||||||
|
bool autoApplyPending = false;
|
||||||
|
};
|
||||||
|
|
||||||
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
QHash<TextEditor::TextDocument *, QQueue<ChangeInfo>> m_documentChanges;
|
||||||
|
QHash<QString, FileEdit> m_fileEdits;
|
||||||
|
QHash<QString, RequestEdits> m_requestEdits; // requestId → ordered edits
|
||||||
|
QUndoStack *m_undoStack;
|
||||||
|
mutable QMutex m_mutex;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@ -51,4 +51,23 @@ bool ProjectUtils::isFileInProject(const QString &filePath)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ProjectUtils::findFileInProject(const QString &filename)
|
||||||
|
{
|
||||||
|
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||||
|
|
||||||
|
for (auto project : projects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||||
|
for (const auto &projectFile : std::as_const(projectFiles)) {
|
||||||
|
if (projectFile.fileName() == filename) {
|
||||||
|
return projectFile.toFSPathString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@ -41,6 +41,17 @@ public:
|
|||||||
* @return true if file is part of any open project, false otherwise
|
* @return true if file is part of any open project, false otherwise
|
||||||
*/
|
*/
|
||||||
static bool isFileInProject(const QString &filePath);
|
static bool isFileInProject(const QString &filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find a file in open projects by filename
|
||||||
|
*
|
||||||
|
* Searches all open projects for a file matching the given filename.
|
||||||
|
* If multiple files with the same name exist, returns the first match.
|
||||||
|
*
|
||||||
|
* @param filename File name to search for (e.g., "main.cpp")
|
||||||
|
* @return Absolute file path if found, empty string otherwise
|
||||||
|
*/
|
||||||
|
static QString findFileInProject(const QString &filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Context
|
} // namespace QodeAssist::Context
|
||||||
|
|||||||
@ -7,6 +7,7 @@ add_library(QodeAssistSettings STATIC
|
|||||||
SettingsTr.hpp
|
SettingsTr.hpp
|
||||||
CodeCompletionSettings.hpp CodeCompletionSettings.cpp
|
CodeCompletionSettings.hpp CodeCompletionSettings.cpp
|
||||||
ChatAssistantSettings.hpp ChatAssistantSettings.cpp
|
ChatAssistantSettings.hpp ChatAssistantSettings.cpp
|
||||||
|
ToolsSettings.hpp ToolsSettings.cpp
|
||||||
SettingsDialog.hpp SettingsDialog.cpp
|
SettingsDialog.hpp SettingsDialog.cpp
|
||||||
ProjectSettings.hpp ProjectSettings.cpp
|
ProjectSettings.hpp ProjectSettings.cpp
|
||||||
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
|
ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp
|
||||||
|
|||||||
@ -205,40 +205,6 @@ GeneralSettings::GeneralSettings()
|
|||||||
caTemplateDescription.setDefaultValue("");
|
caTemplateDescription.setDefaultValue("");
|
||||||
caTemplateDescription.setLabelText(TrConstants::CURRENT_TEMPLATE_DESCRIPTION);
|
caTemplateDescription.setLabelText(TrConstants::CURRENT_TEMPLATE_DESCRIPTION);
|
||||||
|
|
||||||
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(
|
|
||||||
Tr::tr("Allow tools to read files from disk (project files, open editors)"));
|
|
||||||
allowFileSystemRead.setDefaultValue(true);
|
|
||||||
|
|
||||||
allowFileSystemWrite.setSettingsKey(Constants::CA_ALLOW_FILE_SYSTEM_WRITE);
|
|
||||||
allowFileSystemWrite.setLabelText(Tr::tr("Allow File System Write Access for tools"));
|
|
||||||
allowFileSystemWrite.setToolTip(
|
|
||||||
Tr::tr("Allow tools to write and modify files on disk (WARNING: Use with caution!)"));
|
|
||||||
allowFileSystemWrite.setDefaultValue(false);
|
|
||||||
|
|
||||||
allowAccessOutsideProject.setSettingsKey(Constants::CA_ALLOW_ACCESS_OUTSIDE_PROJECT);
|
|
||||||
allowAccessOutsideProject.setLabelText(Tr::tr("Allow file access outside project"));
|
|
||||||
allowAccessOutsideProject.setToolTip(
|
|
||||||
Tr::tr("Allow tools to access (read/write) files outside the project scope (system headers, Qt files, external libraries)"));
|
|
||||||
allowAccessOutsideProject.setDefaultValue(true);
|
|
||||||
|
|
||||||
autoApplyFileEdits.setSettingsKey(Constants::CA_AUTO_APPLY_FILE_EDITS);
|
|
||||||
autoApplyFileEdits.setLabelText(Tr::tr("Automatically apply file edits"));
|
|
||||||
autoApplyFileEdits.setToolTip(
|
|
||||||
Tr::tr("When enabled, file edits suggested by AI will be applied automatically. "
|
|
||||||
"When disabled, you will need to manually approve each edit."));
|
|
||||||
autoApplyFileEdits.setDefaultValue(false);
|
|
||||||
|
|
||||||
readSettings();
|
readSettings();
|
||||||
|
|
||||||
Logger::instance().setLoggingEnabled(enableLogging());
|
Logger::instance().setLoggingEnabled(enableLogging());
|
||||||
@ -286,9 +252,6 @@ GeneralSettings::GeneralSettings()
|
|||||||
title(TrConstants::CHAT_ASSISTANT),
|
title(TrConstants::CHAT_ASSISTANT),
|
||||||
Column{
|
Column{
|
||||||
caGrid,
|
caGrid,
|
||||||
Column{
|
|
||||||
useTools, allowFileSystemRead, allowFileSystemWrite, allowAccessOutsideProject,
|
|
||||||
/*autoApplyFileEdits*/},
|
|
||||||
caTemplateDescription}};
|
caTemplateDescription}};
|
||||||
|
|
||||||
auto rootLayout = Column{
|
auto rootLayout = Column{
|
||||||
@ -543,11 +506,6 @@ void GeneralSettings::resetPageToDefaults()
|
|||||||
resetAspect(ccPreset1CustomEndpoint);
|
resetAspect(ccPreset1CustomEndpoint);
|
||||||
resetAspect(caEndpointMode);
|
resetAspect(caEndpointMode);
|
||||||
resetAspect(caCustomEndpoint);
|
resetAspect(caCustomEndpoint);
|
||||||
resetAspect(useTools);
|
|
||||||
resetAspect(allowFileSystemRead);
|
|
||||||
resetAspect(allowFileSystemWrite);
|
|
||||||
resetAspect(allowAccessOutsideProject);
|
|
||||||
resetAspect(autoApplyFileEdits);
|
|
||||||
writeSettings();
|
writeSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,12 +100,6 @@ public:
|
|||||||
Utils::StringAspect caStatus{this};
|
Utils::StringAspect caStatus{this};
|
||||||
ButtonAspect caTest{this};
|
ButtonAspect caTest{this};
|
||||||
|
|
||||||
Utils::BoolAspect useTools{this};
|
|
||||||
Utils::BoolAspect allowFileSystemRead{this};
|
|
||||||
Utils::BoolAspect allowFileSystemWrite{this};
|
|
||||||
Utils::BoolAspect allowAccessOutsideProject{this};
|
|
||||||
Utils::BoolAspect autoApplyFileEdits{this};
|
|
||||||
|
|
||||||
Utils::StringAspect caTemplateDescription{this};
|
Utils::StringAspect caTemplateDescription{this};
|
||||||
|
|
||||||
void showSelectionDialog(
|
void showSelectionDialog(
|
||||||
|
|||||||
@ -89,6 +89,7 @@ const char CA_USE_TOOLS[] = "QodeAssist.caUseTools";
|
|||||||
const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead";
|
const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead";
|
||||||
const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite";
|
const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite";
|
||||||
const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject";
|
||||||
|
const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool";
|
||||||
|
|
||||||
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
|
||||||
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||||
@ -96,13 +97,14 @@ const char QODE_ASSIST_CODE_COMPLETION_SETTINGS_PAGE_ID[]
|
|||||||
= "QodeAssist.2CodeCompletionSettingsPageId";
|
= "QodeAssist.2CodeCompletionSettingsPageId";
|
||||||
const char QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID[]
|
const char QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID[]
|
||||||
= "QodeAssist.3ChatAssistantSettingsPageId";
|
= "QodeAssist.3ChatAssistantSettingsPageId";
|
||||||
const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.4CustomPromptSettingsPageId";
|
const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.4ToolsSettingsPageId";
|
||||||
|
const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.5CustomPromptSettingsPageId";
|
||||||
|
|
||||||
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
|
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
|
||||||
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";
|
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";
|
||||||
|
|
||||||
// Provider Settings Page ID
|
// Provider Settings Page ID
|
||||||
const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.5ProviderSettingsPageId";
|
const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.6ProviderSettingsPageId";
|
||||||
|
|
||||||
// Provider API Keys
|
// Provider API Keys
|
||||||
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
|
const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey";
|
||||||
|
|||||||
157
settings/ToolsSettings.cpp
Normal file
157
settings/ToolsSettings.cpp
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ToolsSettings.hpp"
|
||||||
|
|
||||||
|
#include <coreplugin/dialogs/ioptionspage.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <QMessageBox>
|
||||||
|
|
||||||
|
#include "SettingsConstants.hpp"
|
||||||
|
#include "SettingsTr.hpp"
|
||||||
|
#include "SettingsUtils.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
ToolsSettings &toolsSettings()
|
||||||
|
{
|
||||||
|
static ToolsSettings settings;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolsSettings::ToolsSettings()
|
||||||
|
{
|
||||||
|
setAutoApply(false);
|
||||||
|
|
||||||
|
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(
|
||||||
|
Tr::tr("Allow tools to read files from disk (project files, open editors)"));
|
||||||
|
allowFileSystemRead.setDefaultValue(true);
|
||||||
|
|
||||||
|
allowFileSystemWrite.setSettingsKey(Constants::CA_ALLOW_FILE_SYSTEM_WRITE);
|
||||||
|
allowFileSystemWrite.setLabelText(Tr::tr("Allow File System Write Access for tools"));
|
||||||
|
allowFileSystemWrite.setToolTip(
|
||||||
|
Tr::tr("Allow tools to write and modify files on disk (WARNING: Use with caution!)"));
|
||||||
|
allowFileSystemWrite.setDefaultValue(false);
|
||||||
|
|
||||||
|
allowAccessOutsideProject.setSettingsKey(Constants::CA_ALLOW_ACCESS_OUTSIDE_PROJECT);
|
||||||
|
allowAccessOutsideProject.setLabelText(Tr::tr("Allow file access outside project"));
|
||||||
|
allowAccessOutsideProject.setToolTip(
|
||||||
|
Tr::tr("Allow tools to access (read/write) files outside the project scope (system "
|
||||||
|
"headers, Qt files, external libraries)"));
|
||||||
|
allowAccessOutsideProject.setDefaultValue(true);
|
||||||
|
|
||||||
|
autoApplyFileEdits.setSettingsKey(Constants::CA_AUTO_APPLY_FILE_EDITS);
|
||||||
|
autoApplyFileEdits.setLabelText(Tr::tr("Automatically apply file edits"));
|
||||||
|
autoApplyFileEdits.setToolTip(
|
||||||
|
Tr::tr("When enabled, file edits suggested by AI will be applied automatically. "
|
||||||
|
"When disabled, you will need to manually approve each edit."));
|
||||||
|
autoApplyFileEdits.setDefaultValue(false);
|
||||||
|
|
||||||
|
enableEditFileTool.setSettingsKey(Constants::CA_ENABLE_EDIT_FILE_TOOL);
|
||||||
|
enableEditFileTool.setLabelText(Tr::tr("Enable Edit File Tool (Experimental)"));
|
||||||
|
enableEditFileTool.setToolTip(
|
||||||
|
Tr::tr("Enable the experimental edit_file tool that allows AI to directly modify files. "
|
||||||
|
"This feature is under testing and may have unexpected behavior."));
|
||||||
|
enableEditFileTool.setDefaultValue(false);
|
||||||
|
|
||||||
|
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
|
||||||
|
|
||||||
|
readSettings();
|
||||||
|
|
||||||
|
setupConnections();
|
||||||
|
|
||||||
|
setLayouter([this]() {
|
||||||
|
using namespace Layouting;
|
||||||
|
|
||||||
|
return Column{
|
||||||
|
Row{Stretch{1}, resetToDefaults},
|
||||||
|
Space{8},
|
||||||
|
Group{
|
||||||
|
title(Tr::tr("Tool Settings")),
|
||||||
|
Column{
|
||||||
|
useTools,
|
||||||
|
Space{8},
|
||||||
|
allowFileSystemRead,
|
||||||
|
allowFileSystemWrite,
|
||||||
|
allowAccessOutsideProject,
|
||||||
|
}},
|
||||||
|
Space{8},
|
||||||
|
Group{
|
||||||
|
title(Tr::tr("Experimental Features")),
|
||||||
|
Column{enableEditFileTool, autoApplyFileEdits}},
|
||||||
|
Stretch{1}};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolsSettings::setupConnections()
|
||||||
|
{
|
||||||
|
connect(
|
||||||
|
&resetToDefaults,
|
||||||
|
&ButtonAspect::clicked,
|
||||||
|
this,
|
||||||
|
&ToolsSettings::resetSettingsToDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToolsSettings::resetSettingsToDefaults()
|
||||||
|
{
|
||||||
|
QMessageBox::StandardButton reply;
|
||||||
|
reply = QMessageBox::question(
|
||||||
|
Core::ICore::dialogParent(),
|
||||||
|
Tr::tr("Reset Settings"),
|
||||||
|
Tr::tr("Are you sure you want to reset all settings to default values?"),
|
||||||
|
QMessageBox::Yes | QMessageBox::No);
|
||||||
|
|
||||||
|
if (reply == QMessageBox::Yes) {
|
||||||
|
resetAspect(useTools);
|
||||||
|
resetAspect(allowFileSystemRead);
|
||||||
|
resetAspect(allowFileSystemWrite);
|
||||||
|
resetAspect(allowAccessOutsideProject);
|
||||||
|
resetAspect(autoApplyFileEdits);
|
||||||
|
resetAspect(enableEditFileTool);
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolsSettingsPage : public Core::IOptionsPage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ToolsSettingsPage()
|
||||||
|
{
|
||||||
|
setId(Constants::QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID);
|
||||||
|
setDisplayName(Tr::tr("Tools"));
|
||||||
|
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
|
||||||
|
setSettingsProvider([] { return &toolsSettings(); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolsSettingsPage toolsSettingsPage;
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
51
settings/ToolsSettings.hpp
Normal file
51
settings/ToolsSettings.hpp
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024-2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <utils/aspects.h>
|
||||||
|
|
||||||
|
#include "ButtonAspect.hpp"
|
||||||
|
|
||||||
|
namespace QodeAssist::Settings {
|
||||||
|
|
||||||
|
class ToolsSettings : public Utils::AspectContainer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ToolsSettings();
|
||||||
|
|
||||||
|
ButtonAspect resetToDefaults{this};
|
||||||
|
|
||||||
|
Utils::BoolAspect useTools{this};
|
||||||
|
Utils::BoolAspect allowFileSystemRead{this};
|
||||||
|
Utils::BoolAspect allowFileSystemWrite{this};
|
||||||
|
Utils::BoolAspect allowAccessOutsideProject{this};
|
||||||
|
|
||||||
|
// Experimental features
|
||||||
|
Utils::BoolAspect enableEditFileTool{this};
|
||||||
|
Utils::BoolAspect autoApplyFileEdits{this};
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setupConnections();
|
||||||
|
void resetSettingsToDefaults();
|
||||||
|
};
|
||||||
|
|
||||||
|
ToolsSettings &toolsSettings();
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Settings
|
||||||
@ -49,7 +49,7 @@ QString BuildProjectTool::stringName() const
|
|||||||
QString BuildProjectTool::description() const
|
QString BuildProjectTool::description() const
|
||||||
{
|
{
|
||||||
return "Build the current project in Qt Creator. "
|
return "Build the current project in Qt Creator. "
|
||||||
"Returns build status and any compilation errors/warnings. "
|
"No returns simultaneously build status and any compilation errors/warnings. "
|
||||||
"Optional 'rebuild' parameter: set to true to force a clean rebuild (default: false).";
|
"Optional 'rebuild' parameter: set to true to force a clean rebuild (default: false).";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
#include <context/ProjectUtils.hpp>
|
#include <context/ProjectUtils.hpp>
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
|
#include <settings/ToolsSettings.hpp>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@ -103,7 +104,7 @@ QFuture<QString> CreateNewFileTool::executeAsync(const QJsonObject &input)
|
|||||||
bool isInProject = Context::ProjectUtils::isFileInProject(absolutePath);
|
bool isInProject = Context::ProjectUtils::isFileInProject(absolutePath);
|
||||||
|
|
||||||
if (!isInProject) {
|
if (!isInProject) {
|
||||||
const auto &settings = Settings::generalSettings();
|
const auto &settings = Settings::toolsSettings();
|
||||||
if (!settings.allowAccessOutsideProject()) {
|
if (!settings.allowAccessOutsideProject()) {
|
||||||
throw ToolRuntimeError(
|
throw ToolRuntimeError(
|
||||||
QString("Error: File path '%1' is not within the current project. "
|
QString("Error: File path '%1' is not within the current project. "
|
||||||
|
|||||||
215
tools/EditFileTool.cpp
Normal file
215
tools/EditFileTool.cpp
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "EditFileTool.hpp"
|
||||||
|
#include "ToolExceptions.hpp"
|
||||||
|
|
||||||
|
#include <context/ChangesManager.h>
|
||||||
|
#include <context/ProjectUtils.hpp>
|
||||||
|
#include <logger/Logger.hpp>
|
||||||
|
#include <settings/GeneralSettings.hpp>
|
||||||
|
#include <settings/ToolsSettings.hpp>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
|
||||||
|
namespace QodeAssist::Tools {
|
||||||
|
|
||||||
|
EditFileTool::EditFileTool(QObject *parent)
|
||||||
|
: BaseTool(parent)
|
||||||
|
, m_ignoreManager(new Context::IgnoreManager(this))
|
||||||
|
{}
|
||||||
|
|
||||||
|
QString EditFileTool::name() const
|
||||||
|
{
|
||||||
|
return "edit_file";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EditFileTool::stringName() const
|
||||||
|
{
|
||||||
|
return {"Editing file"};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "
|
||||||
|
"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. "
|
||||||
|
"If old_content is empty, new_content will be appended to the end of the file.";
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject EditFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
|
||||||
|
{
|
||||||
|
QJsonObject properties;
|
||||||
|
|
||||||
|
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";
|
||||||
|
properties["filename"] = filenameProperty;
|
||||||
|
|
||||||
|
QJsonObject oldContentProperty;
|
||||||
|
oldContentProperty["type"] = "string";
|
||||||
|
oldContentProperty["description"]
|
||||||
|
= "The exact content to find and replace. Must match exactly (including whitespace). "
|
||||||
|
"If empty, new_content will be appended to the end of the file";
|
||||||
|
properties["old_content"] = oldContentProperty;
|
||||||
|
|
||||||
|
QJsonObject newContentProperty;
|
||||||
|
newContentProperty["type"] = "string";
|
||||||
|
newContentProperty["description"] = "The new content to replace the old content with";
|
||||||
|
properties["new_content"] = newContentProperty;
|
||||||
|
|
||||||
|
QJsonObject definition;
|
||||||
|
definition["type"] = "object";
|
||||||
|
definition["properties"] = properties;
|
||||||
|
QJsonArray required;
|
||||||
|
required.append("filename");
|
||||||
|
required.append("new_content");
|
||||||
|
definition["required"] = required;
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case LLMCore::ToolSchemaFormat::OpenAI:
|
||||||
|
return customizeForOpenAI(definition);
|
||||||
|
case LLMCore::ToolSchemaFormat::Claude:
|
||||||
|
return customizeForClaude(definition);
|
||||||
|
case LLMCore::ToolSchemaFormat::Ollama:
|
||||||
|
return customizeForOllama(definition);
|
||||||
|
case LLMCore::ToolSchemaFormat::Google:
|
||||||
|
return customizeForGoogle(definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
LLMCore::ToolPermissions EditFileTool::requiredPermissions() const
|
||||||
|
{
|
||||||
|
return LLMCore::ToolPermission::FileSystemWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFuture<QString> EditFileTool::executeAsync(const QJsonObject &input)
|
||||||
|
{
|
||||||
|
return QtConcurrent::run([this, input]() -> QString {
|
||||||
|
QString filename = input["filename"].toString().trimmed();
|
||||||
|
QString oldContent = input["old_content"].toString();
|
||||||
|
QString newContent = input["new_content"].toString();
|
||||||
|
QString requestId = input["_request_id"].toString();
|
||||||
|
|
||||||
|
if (filename.isEmpty()) {
|
||||||
|
throw ToolInvalidArgument("'filename' parameter is required and cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContent.isEmpty()) {
|
||||||
|
throw ToolInvalidArgument("'new_content' parameter is required and cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
QString filePath;
|
||||||
|
QFileInfo fileInfo(filename);
|
||||||
|
|
||||||
|
if (fileInfo.isAbsolute() && fileInfo.exists()) {
|
||||||
|
filePath = filename;
|
||||||
|
} else {
|
||||||
|
FileSearchUtils::FileMatch match = FileSearchUtils::findBestMatch(
|
||||||
|
filename, QString(), 10, m_ignoreManager);
|
||||||
|
|
||||||
|
if (match.absolutePath.isEmpty()) {
|
||||||
|
throw ToolRuntimeError(
|
||||||
|
QString("File '%1' not found in project. "
|
||||||
|
"Please provide a valid filename or absolute path.")
|
||||||
|
.arg(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = match.absolutePath;
|
||||||
|
LOG_MESSAGE(QString("EditFileTool: Found file '%1' at '%2'")
|
||||||
|
.arg(filename, filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.exists()) {
|
||||||
|
throw ToolRuntimeError(QString("File does not exist: %1").arg(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo finalFileInfo(filePath);
|
||||||
|
if (!finalFileInfo.isWritable()) {
|
||||||
|
throw ToolRuntimeError(
|
||||||
|
QString("File is not writable (read-only or permission denied): %1").arg(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInProject = Context::ProjectUtils::isFileInProject(filePath);
|
||||||
|
if (!isInProject) {
|
||||||
|
const auto &settings = Settings::toolsSettings();
|
||||||
|
if (!settings.allowAccessOutsideProject()) {
|
||||||
|
throw ToolRuntimeError(
|
||||||
|
QString("File path '%1' is not within the current project. "
|
||||||
|
"Enable 'Allow file access outside project' in settings to edit files outside the project.")
|
||||||
|
.arg(filePath));
|
||||||
|
}
|
||||||
|
LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString editId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
bool autoApply = Settings::toolsSettings().autoApplyFileEdits();
|
||||||
|
|
||||||
|
Context::ChangesManager::instance().addFileEdit(
|
||||||
|
editId,
|
||||||
|
filePath,
|
||||||
|
oldContent,
|
||||||
|
newContent,
|
||||||
|
autoApply,
|
||||||
|
false,
|
||||||
|
requestId
|
||||||
|
);
|
||||||
|
|
||||||
|
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
||||||
|
QString status = "pending";
|
||||||
|
if (edit.status == Context::ChangesManager::Applied) {
|
||||||
|
status = "applied";
|
||||||
|
} else if (edit.status == Context::ChangesManager::Rejected) {
|
||||||
|
status = "rejected";
|
||||||
|
} else if (edit.status == Context::ChangesManager::Archived) {
|
||||||
|
status = "archived";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString statusMessage = edit.statusMessage;
|
||||||
|
|
||||||
|
QJsonObject result;
|
||||||
|
result["edit_id"] = editId;
|
||||||
|
result["file"] = filePath;
|
||||||
|
result["old_content"] = oldContent;
|
||||||
|
result["new_content"] = newContent;
|
||||||
|
result["status"] = status;
|
||||||
|
result["status_message"] = statusMessage;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("File edit created: %1 (ID: %2, Status: %3, Deferred: %4)")
|
||||||
|
.arg(filePath, editId, status, requestId.isEmpty() ? QString("no") : QString("yes")));
|
||||||
|
|
||||||
|
QString resultStr = "QODEASSIST_FILE_EDIT:"
|
||||||
|
+ QString::fromUtf8(QJsonDocument(result).toJson(QJsonDocument::Compact));
|
||||||
|
return resultStr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Tools
|
||||||
|
|
||||||
48
tools/EditFileTool.hpp
Normal file
48
tools/EditFileTool.hpp
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "FileSearchUtils.hpp"
|
||||||
|
|
||||||
|
#include <context/IgnoreManager.hpp>
|
||||||
|
#include <llmcore/BaseTool.hpp>
|
||||||
|
|
||||||
|
namespace QodeAssist::Tools {
|
||||||
|
|
||||||
|
class EditFileTool : public LLMCore::BaseTool
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit EditFileTool(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString name() const override;
|
||||||
|
QString stringName() const override;
|
||||||
|
QString description() const override;
|
||||||
|
QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
|
||||||
|
LLMCore::ToolPermissions requiredPermissions() const override;
|
||||||
|
|
||||||
|
QFuture<QString> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Context::IgnoreManager *m_ignoreManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Tools
|
||||||
|
|
||||||
258
tools/FileSearchUtils.cpp
Normal file
258
tools/FileSearchUtils.cpp
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "FileSearchUtils.hpp"
|
||||||
|
|
||||||
|
#include <context/ProjectUtils.hpp>
|
||||||
|
#include <logger/Logger.hpp>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
#include <settings/GeneralSettings.hpp>
|
||||||
|
#include <settings/ToolsSettings.hpp>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
namespace QodeAssist::Tools {
|
||||||
|
|
||||||
|
FileSearchUtils::FileMatch FileSearchUtils::findBestMatch(
|
||||||
|
const QString &query,
|
||||||
|
const QString &filePattern,
|
||||||
|
int maxResults,
|
||||||
|
Context::IgnoreManager *ignoreManager)
|
||||||
|
{
|
||||||
|
QList<FileMatch> candidates;
|
||||||
|
auto projects = ProjectExplorer::ProjectManager::projects();
|
||||||
|
|
||||||
|
if (projects.isEmpty()) {
|
||||||
|
return FileMatch{};
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo queryInfo(query);
|
||||||
|
if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) {
|
||||||
|
FileMatch match;
|
||||||
|
match.absolutePath = queryInfo.canonicalFilePath();
|
||||||
|
|
||||||
|
for (auto project : projects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
QString projectDir = project->projectDirectory().path();
|
||||||
|
if (match.absolutePath.startsWith(projectDir)) {
|
||||||
|
match.relativePath = QDir(projectDir).relativeFilePath(match.absolutePath);
|
||||||
|
match.projectName = project->displayName();
|
||||||
|
match.matchType = MatchType::ExactName;
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match.relativePath = queryInfo.fileName();
|
||||||
|
match.projectName = "External";
|
||||||
|
match.matchType = MatchType::ExactName;
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString lowerQuery = query.toLower();
|
||||||
|
|
||||||
|
for (auto project : projects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||||
|
QString projectDir = project->projectDirectory().path();
|
||||||
|
QString projectName = project->displayName();
|
||||||
|
|
||||||
|
for (const auto &filePath : projectFiles) {
|
||||||
|
QString absolutePath = filePath.path();
|
||||||
|
|
||||||
|
if (ignoreManager && ignoreManager->shouldIgnore(absolutePath, project))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QFileInfo fileInfo(absolutePath);
|
||||||
|
QString fileName = fileInfo.fileName();
|
||||||
|
|
||||||
|
if (!filePattern.isEmpty() && !matchesFilePattern(fileName, filePattern))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||||
|
|
||||||
|
FileMatch match;
|
||||||
|
match.absolutePath = absolutePath;
|
||||||
|
match.relativePath = relativePath;
|
||||||
|
match.projectName = projectName;
|
||||||
|
|
||||||
|
QString lowerFileName = fileName.toLower();
|
||||||
|
QString lowerRelativePath = relativePath.toLower();
|
||||||
|
|
||||||
|
if (lowerFileName == lowerQuery) {
|
||||||
|
match.matchType = MatchType::ExactName;
|
||||||
|
candidates.append(match);
|
||||||
|
} else if (lowerRelativePath.contains(lowerQuery)) {
|
||||||
|
match.matchType = MatchType::PathMatch;
|
||||||
|
candidates.append(match);
|
||||||
|
} else if (lowerFileName.contains(lowerQuery)) {
|
||||||
|
match.matchType = MatchType::PartialName;
|
||||||
|
candidates.append(match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.isEmpty() || candidates.first().matchType != MatchType::ExactName) {
|
||||||
|
for (auto project : projects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QString projectDir = project->projectDirectory().path();
|
||||||
|
QString projectName = project->displayName();
|
||||||
|
int depth = 0;
|
||||||
|
searchInFileSystem(
|
||||||
|
projectDir,
|
||||||
|
lowerQuery,
|
||||||
|
projectName,
|
||||||
|
projectDir,
|
||||||
|
project,
|
||||||
|
candidates,
|
||||||
|
maxResults,
|
||||||
|
depth,
|
||||||
|
5,
|
||||||
|
ignoreManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
return FileMatch{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(candidates.begin(), candidates.end());
|
||||||
|
return candidates.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileSearchUtils::searchInFileSystem(
|
||||||
|
const QString &dirPath,
|
||||||
|
const QString &query,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString &projectDir,
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
QList<FileMatch> &matches,
|
||||||
|
int maxResults,
|
||||||
|
int ¤tDepth,
|
||||||
|
int maxDepth,
|
||||||
|
Context::IgnoreManager *ignoreManager)
|
||||||
|
{
|
||||||
|
if (currentDepth >= maxDepth || matches.size() >= maxResults)
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentDepth++;
|
||||||
|
QDir dir(dirPath);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
currentDepth--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const auto &entry : entries) {
|
||||||
|
if (matches.size() >= maxResults)
|
||||||
|
break;
|
||||||
|
|
||||||
|
QString absolutePath = entry.absoluteFilePath();
|
||||||
|
|
||||||
|
if (ignoreManager && ignoreManager->shouldIgnore(absolutePath, project))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QString fileName = entry.fileName();
|
||||||
|
|
||||||
|
if (entry.isDir()) {
|
||||||
|
searchInFileSystem(
|
||||||
|
absolutePath,
|
||||||
|
query,
|
||||||
|
projectName,
|
||||||
|
projectDir,
|
||||||
|
project,
|
||||||
|
matches,
|
||||||
|
maxResults,
|
||||||
|
currentDepth,
|
||||||
|
maxDepth,
|
||||||
|
ignoreManager);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString lowerFileName = fileName.toLower();
|
||||||
|
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||||
|
QString lowerRelativePath = relativePath.toLower();
|
||||||
|
|
||||||
|
FileMatch match;
|
||||||
|
match.absolutePath = absolutePath;
|
||||||
|
match.relativePath = relativePath;
|
||||||
|
match.projectName = projectName;
|
||||||
|
|
||||||
|
if (lowerFileName == query) {
|
||||||
|
match.matchType = MatchType::ExactName;
|
||||||
|
matches.append(match);
|
||||||
|
} else if (lowerRelativePath.contains(query)) {
|
||||||
|
match.matchType = MatchType::PathMatch;
|
||||||
|
matches.append(match);
|
||||||
|
} else if (lowerFileName.contains(query)) {
|
||||||
|
match.matchType = MatchType::PartialName;
|
||||||
|
matches.append(match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDepth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileSearchUtils::matchesFilePattern(const QString &fileName, const QString &pattern)
|
||||||
|
{
|
||||||
|
if (pattern.isEmpty())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (pattern.startsWith("*.")) {
|
||||||
|
QString extension = pattern.mid(1);
|
||||||
|
return fileName.endsWith(extension, Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName.compare(pattern, Qt::CaseInsensitive) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileSearchUtils::readFileContent(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString canonicalPath = QFileInfo(filePath).canonicalFilePath();
|
||||||
|
bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
|
||||||
|
|
||||||
|
if (!isInProject) {
|
||||||
|
const auto &settings = Settings::toolsSettings();
|
||||||
|
if (!settings.allowAccessOutsideProject()) {
|
||||||
|
LOG_MESSAGE(QString("Access denied to file outside project: %1").arg(canonicalPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream stream(&file);
|
||||||
|
stream.setAutoDetectUnicode(true);
|
||||||
|
QString content = stream.readAll();
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Tools
|
||||||
152
tools/FileSearchUtils.hpp
Normal file
152
tools/FileSearchUtils.hpp
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Petr Mironychev
|
||||||
|
*
|
||||||
|
* This file is part of QodeAssist.
|
||||||
|
*
|
||||||
|
* QodeAssist is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* QodeAssist is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <context/IgnoreManager.hpp>
|
||||||
|
#include <QString>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
namespace ProjectExplorer {
|
||||||
|
class Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace QodeAssist::Tools {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Utility class for file searching and reading operations
|
||||||
|
*
|
||||||
|
* Provides common functionality for file operations used by various tools:
|
||||||
|
* - Fuzzy file searching with multiple match strategies
|
||||||
|
* - File pattern matching (e.g., *.cpp, *.h)
|
||||||
|
* - Secure file content reading with project boundary checks
|
||||||
|
* - Integration with IgnoreManager for respecting .qodeassistignore
|
||||||
|
*/
|
||||||
|
class FileSearchUtils
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Match quality levels for file search results
|
||||||
|
*/
|
||||||
|
enum class MatchType {
|
||||||
|
ExactName, ///< Exact filename match (highest priority)
|
||||||
|
PathMatch, ///< Query found in relative path
|
||||||
|
PartialName ///< Query found in filename (lowest priority)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Represents a file search result with metadata
|
||||||
|
*/
|
||||||
|
struct FileMatch
|
||||||
|
{
|
||||||
|
QString absolutePath; ///< Full absolute path to the file
|
||||||
|
QString relativePath; ///< Path relative to project root
|
||||||
|
QString projectName; ///< Name of the project containing the file
|
||||||
|
QString content; ///< File content (if read)
|
||||||
|
MatchType matchType; ///< Quality of the match
|
||||||
|
bool contentRead = false; ///< Whether content has been read
|
||||||
|
QString error; ///< Error message if operation failed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compare matches by quality (for sorting)
|
||||||
|
*/
|
||||||
|
bool operator<(const FileMatch &other) const
|
||||||
|
{
|
||||||
|
return static_cast<int>(matchType) < static_cast<int>(other.matchType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find the best matching file across all open projects
|
||||||
|
*
|
||||||
|
* Search strategy:
|
||||||
|
* 1. Check if query is an absolute path
|
||||||
|
* 2. Search in project source files (exact, path, partial matches)
|
||||||
|
* 3. Search filesystem within project directories (respects .qodeassistignore)
|
||||||
|
*
|
||||||
|
* @param query Filename, partial name, or path to search for (case-insensitive)
|
||||||
|
* @param filePattern Optional file pattern filter (e.g., "*.cpp", "*.h")
|
||||||
|
* @param maxResults Maximum number of candidates to collect
|
||||||
|
* @param ignoreManager IgnoreManager instance for filtering files
|
||||||
|
* @return Best matching file, or empty FileMatch if not found
|
||||||
|
*/
|
||||||
|
static FileMatch findBestMatch(
|
||||||
|
const QString &query,
|
||||||
|
const QString &filePattern = QString(),
|
||||||
|
int maxResults = 10,
|
||||||
|
Context::IgnoreManager *ignoreManager = nullptr);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a filename matches a file pattern
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Wildcard patterns (*.cpp, *.h)
|
||||||
|
* - Exact filename matching
|
||||||
|
* - Empty pattern (matches all)
|
||||||
|
*
|
||||||
|
* @param fileName File name to check
|
||||||
|
* @param pattern Pattern to match against
|
||||||
|
* @return true if filename matches pattern
|
||||||
|
*/
|
||||||
|
static bool matchesFilePattern(const QString &fileName, const QString &pattern);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read file content with security checks
|
||||||
|
*
|
||||||
|
* Performs the following checks:
|
||||||
|
* - File exists and is readable
|
||||||
|
* - Respects project boundary settings (allowAccessOutsideProject)
|
||||||
|
* - Logs access to files outside project scope
|
||||||
|
*
|
||||||
|
* @param filePath Absolute path to file
|
||||||
|
* @return File content as QString, or null QString on error
|
||||||
|
*/
|
||||||
|
static QString readFileContent(const QString &filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Search for files in filesystem directory tree
|
||||||
|
*
|
||||||
|
* Recursively searches a directory for files matching the query.
|
||||||
|
* Respects .qodeassistignore patterns and depth limits.
|
||||||
|
*
|
||||||
|
* @param dirPath Directory to search in
|
||||||
|
* @param query Search query (case-insensitive)
|
||||||
|
* @param projectName Name of the project for metadata
|
||||||
|
* @param projectDir Root directory of the project
|
||||||
|
* @param project Project instance for ignore checking
|
||||||
|
* @param matches Output list to append matches to
|
||||||
|
* @param maxResults Stop after finding this many matches
|
||||||
|
* @param currentDepth Current recursion depth (modified during recursion)
|
||||||
|
* @param maxDepth Maximum recursion depth
|
||||||
|
* @param ignoreManager IgnoreManager instance for filtering files
|
||||||
|
*/
|
||||||
|
static void searchInFileSystem(
|
||||||
|
const QString &dirPath,
|
||||||
|
const QString &query,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString &projectDir,
|
||||||
|
ProjectExplorer::Project *project,
|
||||||
|
QList<FileMatch> &matches,
|
||||||
|
int maxResults,
|
||||||
|
int ¤tDepth,
|
||||||
|
int maxDepth = 5,
|
||||||
|
Context::IgnoreManager *ignoreManager = nullptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Tools
|
||||||
@ -20,17 +20,9 @@
|
|||||||
#include "FindAndReadFileTool.hpp"
|
#include "FindAndReadFileTool.hpp"
|
||||||
#include "ToolExceptions.hpp"
|
#include "ToolExceptions.hpp"
|
||||||
|
|
||||||
#include <context/ProjectUtils.hpp>
|
|
||||||
#include <logger/Logger.hpp>
|
#include <logger/Logger.hpp>
|
||||||
#include <projectexplorer/project.h>
|
|
||||||
#include <projectexplorer/projectmanager.h>
|
|
||||||
#include <settings/GeneralSettings.hpp>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QTextStream>
|
|
||||||
#include <QtConcurrent>
|
#include <QtConcurrent>
|
||||||
|
|
||||||
namespace QodeAssist::Tools {
|
namespace QodeAssist::Tools {
|
||||||
@ -109,14 +101,15 @@ QFuture<QString> FindAndReadFileTool::executeAsync(const QJsonObject &input)
|
|||||||
.arg(query, filePattern.isEmpty() ? "none" : filePattern)
|
.arg(query, filePattern.isEmpty() ? "none" : filePattern)
|
||||||
.arg(readContent));
|
.arg(readContent));
|
||||||
|
|
||||||
FileMatch bestMatch = findBestMatch(query, filePattern, 10);
|
FileSearchUtils::FileMatch bestMatch = FileSearchUtils::findBestMatch(
|
||||||
|
query, filePattern, 10, m_ignoreManager);
|
||||||
|
|
||||||
if (bestMatch.absolutePath.isEmpty()) {
|
if (bestMatch.absolutePath.isEmpty()) {
|
||||||
return QString("No file found matching '%1'").arg(query);
|
return QString("No file found matching '%1'").arg(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readContent) {
|
if (readContent) {
|
||||||
bestMatch.content = readFileContent(bestMatch.absolutePath);
|
bestMatch.content = FileSearchUtils::readFileContent(bestMatch.absolutePath);
|
||||||
if (bestMatch.content.isNull()) {
|
if (bestMatch.content.isNull()) {
|
||||||
bestMatch.error = "Could not read file";
|
bestMatch.error = "Could not read file";
|
||||||
}
|
}
|
||||||
@ -126,221 +119,8 @@ QFuture<QString> FindAndReadFileTool::executeAsync(const QJsonObject &input)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FindAndReadFileTool::FileMatch FindAndReadFileTool::findBestMatch(
|
QString FindAndReadFileTool::formatResult(const FileSearchUtils::FileMatch &match,
|
||||||
const QString &query, const QString &filePattern, int maxResults)
|
bool readContent) const
|
||||||
{
|
|
||||||
QList<FileMatch> candidates;
|
|
||||||
auto projects = ProjectExplorer::ProjectManager::projects();
|
|
||||||
|
|
||||||
if (projects.isEmpty()) {
|
|
||||||
return FileMatch{};
|
|
||||||
}
|
|
||||||
|
|
||||||
QFileInfo queryInfo(query);
|
|
||||||
if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) {
|
|
||||||
FileMatch match;
|
|
||||||
match.absolutePath = queryInfo.canonicalFilePath();
|
|
||||||
|
|
||||||
for (auto project : projects) {
|
|
||||||
if (!project)
|
|
||||||
continue;
|
|
||||||
QString projectDir = project->projectDirectory().path();
|
|
||||||
if (match.absolutePath.startsWith(projectDir)) {
|
|
||||||
match.relativePath = QDir(projectDir).relativeFilePath(match.absolutePath);
|
|
||||||
match.projectName = project->displayName();
|
|
||||||
match.matchType = MatchType::ExactName;
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match.relativePath = queryInfo.fileName();
|
|
||||||
match.projectName = "External";
|
|
||||||
match.matchType = MatchType::ExactName;
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString lowerQuery = query.toLower();
|
|
||||||
|
|
||||||
for (auto project : projects) {
|
|
||||||
if (!project)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
|
||||||
QString projectDir = project->projectDirectory().path();
|
|
||||||
QString projectName = project->displayName();
|
|
||||||
|
|
||||||
for (const auto &filePath : projectFiles) {
|
|
||||||
QString absolutePath = filePath.path();
|
|
||||||
if (m_ignoreManager->shouldIgnore(absolutePath, project))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
QFileInfo fileInfo(absolutePath);
|
|
||||||
QString fileName = fileInfo.fileName();
|
|
||||||
|
|
||||||
if (!filePattern.isEmpty() && !matchesFilePattern(fileName, filePattern))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
|
||||||
|
|
||||||
FileMatch match;
|
|
||||||
match.absolutePath = absolutePath;
|
|
||||||
match.relativePath = relativePath;
|
|
||||||
match.projectName = projectName;
|
|
||||||
|
|
||||||
QString lowerFileName = fileName.toLower();
|
|
||||||
QString lowerRelativePath = relativePath.toLower();
|
|
||||||
|
|
||||||
if (lowerFileName == lowerQuery) {
|
|
||||||
match.matchType = MatchType::ExactName;
|
|
||||||
candidates.append(match);
|
|
||||||
} else if (lowerRelativePath.contains(lowerQuery)) {
|
|
||||||
match.matchType = MatchType::PathMatch;
|
|
||||||
candidates.append(match);
|
|
||||||
} else if (lowerFileName.contains(lowerQuery)) {
|
|
||||||
match.matchType = MatchType::PartialName;
|
|
||||||
candidates.append(match);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.isEmpty() || candidates.first().matchType != MatchType::ExactName) {
|
|
||||||
for (auto project : projects) {
|
|
||||||
if (!project)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
QString projectDir = project->projectDirectory().path();
|
|
||||||
QString projectName = project->displayName();
|
|
||||||
int depth = 0;
|
|
||||||
searchInFileSystem(
|
|
||||||
projectDir,
|
|
||||||
lowerQuery,
|
|
||||||
projectName,
|
|
||||||
projectDir,
|
|
||||||
project,
|
|
||||||
candidates,
|
|
||||||
maxResults,
|
|
||||||
depth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.isEmpty()) {
|
|
||||||
return FileMatch{};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::sort(candidates.begin(), candidates.end());
|
|
||||||
return candidates.first();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FindAndReadFileTool::searchInFileSystem(
|
|
||||||
const QString &dirPath,
|
|
||||||
const QString &query,
|
|
||||||
const QString &projectName,
|
|
||||||
const QString &projectDir,
|
|
||||||
ProjectExplorer::Project *project,
|
|
||||||
QList<FileMatch> &matches,
|
|
||||||
int maxResults,
|
|
||||||
int ¤tDepth,
|
|
||||||
int maxDepth)
|
|
||||||
{
|
|
||||||
if (currentDepth >= maxDepth || matches.size() >= maxResults)
|
|
||||||
return;
|
|
||||||
|
|
||||||
currentDepth++;
|
|
||||||
QDir dir(dirPath);
|
|
||||||
if (!dir.exists()) {
|
|
||||||
currentDepth--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
|
||||||
for (const auto &entry : entries) {
|
|
||||||
if (matches.size() >= maxResults)
|
|
||||||
break;
|
|
||||||
|
|
||||||
QString absolutePath = entry.absoluteFilePath();
|
|
||||||
if (m_ignoreManager->shouldIgnore(absolutePath, project))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
QString fileName = entry.fileName();
|
|
||||||
|
|
||||||
if (entry.isDir()) {
|
|
||||||
searchInFileSystem(
|
|
||||||
absolutePath,
|
|
||||||
query,
|
|
||||||
projectName,
|
|
||||||
projectDir,
|
|
||||||
project,
|
|
||||||
matches,
|
|
||||||
maxResults,
|
|
||||||
currentDepth,
|
|
||||||
maxDepth);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString lowerFileName = fileName.toLower();
|
|
||||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
|
||||||
QString lowerRelativePath = relativePath.toLower();
|
|
||||||
|
|
||||||
FileMatch match;
|
|
||||||
match.absolutePath = absolutePath;
|
|
||||||
match.relativePath = relativePath;
|
|
||||||
match.projectName = projectName;
|
|
||||||
|
|
||||||
if (lowerFileName == query) {
|
|
||||||
match.matchType = MatchType::ExactName;
|
|
||||||
matches.append(match);
|
|
||||||
} else if (lowerRelativePath.contains(query)) {
|
|
||||||
match.matchType = MatchType::PathMatch;
|
|
||||||
matches.append(match);
|
|
||||||
} else if (lowerFileName.contains(query)) {
|
|
||||||
match.matchType = MatchType::PartialName;
|
|
||||||
matches.append(match);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDepth--;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FindAndReadFileTool::matchesFilePattern(const QString &fileName, const QString &pattern) const
|
|
||||||
{
|
|
||||||
if (pattern.isEmpty())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (pattern.startsWith("*.")) {
|
|
||||||
QString extension = pattern.mid(1);
|
|
||||||
return fileName.endsWith(extension, Qt::CaseInsensitive);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName.compare(pattern, Qt::CaseInsensitive) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FindAndReadFileTool::readFileContent(const QString &filePath) const
|
|
||||||
{
|
|
||||||
QFile file(filePath);
|
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString canonicalPath = QFileInfo(filePath).canonicalFilePath();
|
|
||||||
bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
|
|
||||||
|
|
||||||
if (!isInProject) {
|
|
||||||
const auto &settings = Settings::generalSettings();
|
|
||||||
if (!settings.allowAccessOutsideProject()) {
|
|
||||||
LOG_MESSAGE(QString("Access denied to file outside project: %1").arg(canonicalPath));
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextStream stream(&file);
|
|
||||||
stream.setAutoDetectUnicode(true);
|
|
||||||
QString content = stream.readAll();
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FindAndReadFileTool::formatResult(const FileMatch &match, bool readContent) const
|
|
||||||
{
|
{
|
||||||
QString result
|
QString result
|
||||||
= QString("Found file: %1\nAbsolute path: %2").arg(match.relativePath, match.absolutePath);
|
= QString("Found file: %1\nAbsolute path: %2").arg(match.relativePath, match.absolutePath);
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FileSearchUtils.hpp"
|
||||||
|
|
||||||
#include <context/IgnoreManager.hpp>
|
#include <context/IgnoreManager.hpp>
|
||||||
#include <llmcore/BaseTool.hpp>
|
#include <llmcore/BaseTool.hpp>
|
||||||
#include <QFuture>
|
#include <QFuture>
|
||||||
@ -42,38 +44,7 @@ public:
|
|||||||
QFuture<QString> executeAsync(const QJsonObject &input) override;
|
QFuture<QString> executeAsync(const QJsonObject &input) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class MatchType { ExactName, PathMatch, PartialName };
|
QString formatResult(const FileSearchUtils::FileMatch &match, bool readContent) const;
|
||||||
|
|
||||||
struct FileMatch
|
|
||||||
{
|
|
||||||
QString absolutePath;
|
|
||||||
QString relativePath;
|
|
||||||
QString projectName;
|
|
||||||
QString content;
|
|
||||||
MatchType matchType;
|
|
||||||
bool contentRead = false;
|
|
||||||
QString error;
|
|
||||||
|
|
||||||
bool operator<(const FileMatch &other) const
|
|
||||||
{
|
|
||||||
return static_cast<int>(matchType) < static_cast<int>(other.matchType);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
FileMatch findBestMatch(const QString &query, const QString &filePattern, int maxResults);
|
|
||||||
void searchInFileSystem(
|
|
||||||
const QString &dirPath,
|
|
||||||
const QString &query,
|
|
||||||
const QString &projectName,
|
|
||||||
const QString &projectDir,
|
|
||||||
ProjectExplorer::Project *project,
|
|
||||||
QList<FileMatch> &matches,
|
|
||||||
int maxResults,
|
|
||||||
int ¤tDepth,
|
|
||||||
int maxDepth = 5);
|
|
||||||
bool matchesFilePattern(const QString &fileName, const QString &pattern) const;
|
|
||||||
QString readFileContent(const QString &filePath) const;
|
|
||||||
QString formatResult(const FileMatch &match, bool readContent) const;
|
|
||||||
|
|
||||||
Context::IgnoreManager *m_ignoreManager;
|
Context::IgnoreManager *m_ignoreManager;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,11 +21,13 @@
|
|||||||
|
|
||||||
#include "logger/Logger.hpp"
|
#include "logger/Logger.hpp"
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
|
#include <settings/ToolsSettings.hpp>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
|
||||||
#include "BuildProjectTool.hpp"
|
#include "BuildProjectTool.hpp"
|
||||||
#include "CreateNewFileTool.hpp"
|
#include "CreateNewFileTool.hpp"
|
||||||
|
#include "EditFileTool.hpp"
|
||||||
#include "FindAndReadFileTool.hpp"
|
#include "FindAndReadFileTool.hpp"
|
||||||
#include "GetIssuesListTool.hpp"
|
#include "GetIssuesListTool.hpp"
|
||||||
#include "ListProjectFilesTool.hpp"
|
#include "ListProjectFilesTool.hpp"
|
||||||
@ -46,6 +48,7 @@ void ToolsFactory::registerTools()
|
|||||||
registerTool(new ListProjectFilesTool(this));
|
registerTool(new ListProjectFilesTool(this));
|
||||||
registerTool(new GetIssuesListTool(this));
|
registerTool(new GetIssuesListTool(this));
|
||||||
registerTool(new CreateNewFileTool(this));
|
registerTool(new CreateNewFileTool(this));
|
||||||
|
registerTool(new EditFileTool(this));
|
||||||
registerTool(new BuildProjectTool(this));
|
registerTool(new BuildProjectTool(this));
|
||||||
registerTool(new ProjectSearchTool(this));
|
registerTool(new ProjectSearchTool(this));
|
||||||
registerTool(new FindAndReadFileTool(this));
|
registerTool(new FindAndReadFileTool(this));
|
||||||
@ -81,13 +84,17 @@ LLMCore::BaseTool *ToolsFactory::getToolByName(const QString &name) const
|
|||||||
QJsonArray ToolsFactory::getToolsDefinitions(LLMCore::ToolSchemaFormat format) const
|
QJsonArray ToolsFactory::getToolsDefinitions(LLMCore::ToolSchemaFormat format) const
|
||||||
{
|
{
|
||||||
QJsonArray toolsArray;
|
QJsonArray toolsArray;
|
||||||
const auto &settings = Settings::generalSettings();
|
const auto &settings = Settings::toolsSettings();
|
||||||
|
|
||||||
for (auto it = m_tools.constBegin(); it != m_tools.constEnd(); ++it) {
|
for (auto it = m_tools.constBegin(); it != m_tools.constEnd(); ++it) {
|
||||||
if (!it.value()) {
|
if (!it.value()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (it.value()->name() == "edit_file" && !settings.enableEditFileTool()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const auto requiredPerms = it.value()->requiredPermissions();
|
const auto requiredPerms = it.value()->requiredPermissions();
|
||||||
bool hasPermission = true;
|
bool hasPermission = true;
|
||||||
|
|
||||||
|
|||||||
@ -59,7 +59,6 @@ void ToolsManager::executeToolCall(
|
|||||||
|
|
||||||
auto &queue = m_toolQueues[requestId];
|
auto &queue = m_toolQueues[requestId];
|
||||||
|
|
||||||
// Check if tool already exists in queue or completed
|
|
||||||
for (const auto &tool : queue.queue) {
|
for (const auto &tool : queue.queue) {
|
||||||
if (tool.id == toolId) {
|
if (tool.id == toolId) {
|
||||||
LOG_MESSAGE(QString("Tool %1 already in queue for request %2").arg(toolId, requestId));
|
LOG_MESSAGE(QString("Tool %1 already in queue for request %2").arg(toolId, requestId));
|
||||||
@ -73,15 +72,16 @@ void ToolsManager::executeToolCall(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tool to queue
|
QJsonObject modifiedInput = input;
|
||||||
PendingTool pendingTool{toolId, toolName, input, "", false};
|
modifiedInput["_request_id"] = requestId;
|
||||||
|
|
||||||
|
PendingTool pendingTool{toolId, toolName, modifiedInput, "", false};
|
||||||
queue.queue.append(pendingTool);
|
queue.queue.append(pendingTool);
|
||||||
|
|
||||||
LOG_MESSAGE(QString("ToolsManager: Tool %1 added to queue (position %2)")
|
LOG_MESSAGE(QString("ToolsManager: Tool %1 added to queue (position %2)")
|
||||||
.arg(toolName)
|
.arg(toolName)
|
||||||
.arg(queue.queue.size()));
|
.arg(queue.queue.size()));
|
||||||
|
|
||||||
// Start execution if not already running
|
|
||||||
if (!queue.isExecuting) {
|
if (!queue.isExecuting) {
|
||||||
executeNextTool(requestId);
|
executeNextTool(requestId);
|
||||||
}
|
}
|
||||||
@ -95,7 +95,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
|||||||
|
|
||||||
auto &queue = m_toolQueues[requestId];
|
auto &queue = m_toolQueues[requestId];
|
||||||
|
|
||||||
// Check if queue is empty
|
|
||||||
if (queue.queue.isEmpty()) {
|
if (queue.queue.isEmpty()) {
|
||||||
LOG_MESSAGE(QString("ToolsManager: All tools complete for request %1, emitting results")
|
LOG_MESSAGE(QString("ToolsManager: All tools complete for request %1, emitting results")
|
||||||
.arg(requestId));
|
.arg(requestId));
|
||||||
@ -105,7 +104,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get next tool from queue
|
|
||||||
PendingTool tool = queue.queue.takeFirst();
|
PendingTool tool = queue.queue.takeFirst();
|
||||||
queue.isExecuting = true;
|
queue.isExecuting = true;
|
||||||
|
|
||||||
@ -116,7 +114,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
|||||||
auto toolInstance = m_toolsFactory->getToolByName(tool.name);
|
auto toolInstance = m_toolsFactory->getToolByName(tool.name);
|
||||||
if (!toolInstance) {
|
if (!toolInstance) {
|
||||||
LOG_MESSAGE(QString("ToolsManager: Tool not found: %1").arg(tool.name));
|
LOG_MESSAGE(QString("ToolsManager: Tool not found: %1").arg(tool.name));
|
||||||
// Mark as failed and continue to next tool
|
|
||||||
tool.result = QString("Error: Tool not found: %1").arg(tool.name);
|
tool.result = QString("Error: Tool not found: %1").arg(tool.name);
|
||||||
tool.complete = true;
|
tool.complete = true;
|
||||||
queue.completed[tool.id] = tool;
|
queue.completed[tool.id] = tool;
|
||||||
@ -124,7 +121,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store tool in completed map (will be updated when finished)
|
|
||||||
queue.completed[tool.id] = tool;
|
queue.completed[tool.id] = tool;
|
||||||
|
|
||||||
m_toolHandler->executeToolAsync(requestId, tool.id, toolInstance, tool.input);
|
m_toolHandler->executeToolAsync(requestId, tool.id, toolInstance, tool.input);
|
||||||
@ -176,7 +172,6 @@ void ToolsManager::onToolFinished(
|
|||||||
.arg(success ? QString("completed") : QString("failed"))
|
.arg(success ? QString("completed") : QString("failed"))
|
||||||
.arg(requestId));
|
.arg(requestId));
|
||||||
|
|
||||||
// Execute next tool in queue
|
|
||||||
executeNextTool(requestId);
|
executeNextTool(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user