From 43b64b9166144cd15f881d02d85db5ccc01121cc Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Sun, 26 Oct 2025 11:47:16 +0100 Subject: [PATCH] refactor: Simplified edit tool (#242) refactor: Re-work edit file tool --- .gitignore | 3 +- CMakeLists.txt | 2 +- ChatView/ChatModel.cpp | 7 +- ChatView/FileEditItem.cpp | 379 +++++++----------- ChatView/FileEditItem.hpp | 43 +- ChatView/qml/FileEditChangesItem.qml | 45 +-- context/CMakeLists.txt | 1 + context/ProjectUtils.cpp | 54 +++ context/ProjectUtils.hpp | 46 +++ providers/LMStudioProvider.cpp | 6 + providers/LlamaCppProvider.cpp | 6 + providers/MistralAIProvider.cpp | 6 + providers/OllamaProvider.cpp | 12 +- providers/OpenAICompatProvider.cpp | 6 + providers/OpenAIProvider.cpp | 6 + settings/GeneralSettings.cpp | 14 +- settings/GeneralSettings.hpp | 2 +- settings/SettingsConstants.hpp | 2 +- tools/CreateNewFileTool.cpp | 25 +- tools/EditFileTool.cpp | 284 +++++++++++++ ...itProjectFileTool.hpp => EditFileTool.hpp} | 17 +- tools/EditProjectFileTool.cpp | 376 ----------------- tools/FindFileTool.cpp | 32 +- tools/FindFileTool.hpp | 1 - tools/ReadFilesByPathTool.cpp | 34 +- tools/ReadFilesByPathTool.hpp | 1 - tools/ToolsFactory.cpp | 4 +- tools/ToolsManager.cpp | 131 +++--- tools/ToolsManager.hpp | 11 +- 29 files changed, 739 insertions(+), 817 deletions(-) create mode 100644 context/ProjectUtils.cpp create mode 100644 context/ProjectUtils.hpp create mode 100644 tools/EditFileTool.cpp rename tools/{EditProjectFileTool.hpp => EditFileTool.hpp} (70%) delete mode 100644 tools/EditProjectFileTool.cpp diff --git a/.gitignore b/.gitignore index 99471db..0495742 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,5 @@ CMakeLists.txt.user* /build /.qodeassist /.cursor -/.vscode \ No newline at end of file +/.vscode +.qtc_clangd/compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt index af5863c..5de9ddd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,7 +122,7 @@ add_qtc_plugin(QodeAssist tools/ToolsManager.hpp tools/ToolsManager.cpp tools/SearchInProjectTool.hpp tools/SearchInProjectTool.cpp tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp - tools/EditProjectFileTool.hpp tools/EditProjectFileTool.cpp + tools/EditFileTool.hpp tools/EditFileTool.cpp tools/FindSymbolTool.hpp tools/FindSymbolTool.cpp tools/FindFileTool.hpp tools/FindFileTool.cpp tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp diff --git a/ChatView/ChatModel.cpp b/ChatView/ChatModel.cpp index 932d711..5d024d7 100644 --- a/ChatView/ChatModel.cpp +++ b/ChatView/ChatModel.cpp @@ -19,6 +19,7 @@ #include "ChatModel.hpp" #include +#include #include #include #include @@ -324,7 +325,9 @@ void ChatModel::updateToolResult( QString("ERROR: Parsed JSON is not an object, is array=%1").arg(doc.isArray())); } else { QJsonObject editData = doc.object(); - QString editId = editData["edit_id"].toString(); + + // Generate unique edit ID based on timestamp + QString editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch()); LOG_MESSAGE(QString("Adding FileEdit message, editId=%1").arg(editId)); @@ -332,7 +335,7 @@ void ChatModel::updateToolResult( Message fileEditMsg; fileEditMsg.role = ChatRole::FileEdit; fileEditMsg.content = result; - fileEditMsg.id = editId.isEmpty() ? QString("edit_%1").arg(requestId) : editId; + fileEditMsg.id = editId; m_messages.append(fileEditMsg); endInsertRows(); diff --git a/ChatView/FileEditItem.cpp b/ChatView/FileEditItem.cpp index 798af08..647534f 100644 --- a/ChatView/FileEditItem.cpp +++ b/ChatView/FileEditItem.cpp @@ -22,6 +22,7 @@ #include "Logger.hpp" #include "settings/GeneralSettings.hpp" +#include #include #include #include @@ -45,7 +46,6 @@ void FileEditItem::parseFromContent(const QString &content) int markerPos = content.indexOf(marker); if (markerPos == -1) { - LOG_MESSAGE(QString("FileEditItem: ERROR - no marker found")); return; } @@ -56,42 +56,37 @@ void FileEditItem::parseFromContent(const QString &content) QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError) { - LOG_MESSAGE(QString("FileEditItem: JSON parse error at offset %1: %2") - .arg(parseError.offset) - .arg(parseError.errorString())); return; } if (!doc.isObject()) { - LOG_MESSAGE(QString("FileEditItem: ERROR - parsed JSON is not an object")); return; } QJsonObject editData = doc.object(); - m_editId = editData["edit_id"].toString(); - m_filePath = editData["file_path"].toString(); - m_editMode = editData["mode"].toString(); - m_originalContent = editData["original_content"].toString(); - m_newContent = editData["new_content"].toString(); - m_contextBefore = editData["context_before"].toString(); - m_contextAfter = editData["context_after"].toString(); + m_editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch()); + m_mode = editData["mode"].toString(); + m_filePath = editData["filepath"].toString(); + m_newText = editData["new_text"].toString(); m_searchText = editData["search_text"].toString(); - m_lineNumber = editData["line_number"].toInt(-1); + m_lineBefore = editData["line_before"].toString(); + m_lineAfter = editData["line_after"].toString(); - m_addedLines = m_newContent.split('\n').size(); - m_removedLines = m_originalContent.split('\n').size(); + if (m_mode.isEmpty()) { + m_mode = m_searchText.isEmpty() ? "insert_after" : "replace"; + } - LOG_MESSAGE(QString("FileEditItem: parsed successfully, editId=%1, filePath=%2") - .arg(m_editId, m_filePath)); + m_addedLines = m_newText.split('\n').size(); + m_removedLines = m_searchText.isEmpty() ? 0 : m_searchText.split('\n').size(); emit editIdChanged(); + emit modeChanged(); emit filePathChanged(); - emit editModeChanged(); - emit originalContentChanged(); - emit newContentChanged(); - emit contextBeforeChanged(); - emit contextAfterChanged(); + emit searchTextChanged(); + emit newTextChanged(); + emit lineBeforeChanged(); + emit lineAfterChanged(); emit addedLinesChanged(); emit removedLinesChanged(); @@ -153,18 +148,132 @@ void FileEditItem::revertEdit() void FileEditItem::performApply() { - LOG_MESSAGE(QString("FileEditItem: applying edit %1 to %2").arg(m_editId, m_filePath)); - QString currentContent = readFile(m_filePath); - if (currentContent.isNull()) { - rejectWithError(QString("Failed to read file: %1").arg(m_filePath)); - return; - } + m_originalContent = currentContent; - bool success = false; - QString editedContent = applyEditToContent(currentContent, success); - if (!success) { - rejectWithError("Failed to apply edit: could not find context. File may have been modified."); + QString editedContent; + + if (m_mode == "insert_after") { + if (m_lineBefore.isEmpty()) { + editedContent = m_newText + currentContent; + } else { + QList positions; + int pos = 0; + while ((pos = currentContent.indexOf(m_lineBefore, pos)) != -1) { + positions.append(pos); + pos += m_lineBefore.length(); + } + + if (positions.isEmpty()) { + rejectWithError("Failed to apply edit: line_before not found"); + return; + } + + int matchedPosition = -1; + + if (!m_lineAfter.isEmpty()) { + for (int beforePos : positions) { + int afterPos = beforePos + m_lineBefore.length(); + if (afterPos + m_lineAfter.length() <= currentContent.length()) { + QString actualAfter = currentContent.mid(afterPos, m_lineAfter.length()); + if (actualAfter == m_lineAfter) { + matchedPosition = afterPos; + break; + } + } + } + + if (matchedPosition == -1) { + rejectWithError("Failed to apply edit: line_before found but line_after context doesn't match"); + return; + } + } else { + matchedPosition = positions.first() + m_lineBefore.length(); + } + + editedContent = currentContent; + editedContent.insert(matchedPosition, m_newText); + } + + } else if (m_mode == "replace") { + if (m_searchText.isEmpty()) { + rejectWithError("REPLACE mode requires search_text to be specified"); + return; + } + + bool hasLineBefore = !m_lineBefore.isEmpty(); + bool hasLineAfter = !m_lineAfter.isEmpty(); + + QList searchPositions; + int pos = 0; + while ((pos = currentContent.indexOf(m_searchText, pos)) != -1) { + searchPositions.append(pos); + pos += m_searchText.length(); + } + + if (searchPositions.isEmpty()) { + rejectWithError("Failed to apply edit: search_text not found. File may have been modified."); + return; + } + + int matchedPosition = -1; + const int MAX_CONTEXT_DISTANCE = 500; + + for (int searchPos : searchPositions) { + bool beforeMatches = true; + bool afterMatches = true; + + if (hasLineBefore) { + int searchStart = qMax(0, searchPos - MAX_CONTEXT_DISTANCE); + int beforePos = currentContent.lastIndexOf(m_lineBefore, searchPos - 1); + + if (beforePos >= searchStart && beforePos < searchPos) { + beforeMatches = true; + } else { + beforeMatches = false; + } + } + + if (hasLineAfter) { + int afterPos = searchPos + m_searchText.length(); + int searchEnd = qMin(currentContent.length(), afterPos + MAX_CONTEXT_DISTANCE); + int foundAfterPos = currentContent.indexOf(m_lineAfter, afterPos); + + if (foundAfterPos >= afterPos && foundAfterPos < searchEnd) { + afterMatches = true; + } else { + afterMatches = false; + } + } + + bool isMatch = false; + + if (hasLineBefore && hasLineAfter) { + isMatch = beforeMatches && afterMatches; + } else if (hasLineBefore && !hasLineAfter) { + isMatch = beforeMatches; + } else if (!hasLineBefore && hasLineAfter) { + isMatch = afterMatches; + } else { + isMatch = true; + } + + if (isMatch) { + matchedPosition = searchPos; + break; + } + } + + if (matchedPosition == -1) { + rejectWithError("Failed to apply edit: search_text found but context verification failed."); + return; + } + + editedContent = currentContent; + editedContent.replace(matchedPosition, m_searchText.length(), m_newText); + + } else { + rejectWithError(QString("Unknown edit mode: %1").arg(m_mode)); return; } @@ -178,22 +287,7 @@ void FileEditItem::performApply() void FileEditItem::performRevert() { - LOG_MESSAGE(QString("FileEditItem: reverting edit %1 for %2").arg(m_editId, m_filePath)); - - QString currentContent = readFile(m_filePath); - if (currentContent.isNull()) { - rejectWithError(QString("Failed to read file for revert: %1").arg(m_filePath)); - return; - } - - bool success = false; - QString revertedContent = applyReverseEdit(currentContent, success); - if (!success) { - rejectWithError("Failed to revert edit: could not find changes in current file."); - return; - } - - if (!writeFile(m_filePath, revertedContent)) { + if (!writeFile(m_filePath, m_originalContent)) { rejectWithError(QString("Failed to write reverted file: %1").arg(m_filePath)); return; } @@ -203,14 +297,12 @@ void FileEditItem::performRevert() void FileEditItem::rejectWithError(const QString &errorMessage) { - LOG_MESSAGE(errorMessage); setStatus(EditStatus::Rejected); setStatusMessage(errorMessage); } void FileEditItem::finishWithSuccess(EditStatus status, const QString &message) { - LOG_MESSAGE(message); setStatus(status); setStatusMessage(message); } @@ -237,7 +329,6 @@ bool FileEditItem::writeFile(const QString &filePath, const QString &content) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - LOG_MESSAGE(QString("Could not open file for writing: %1").arg(filePath)); return false; } @@ -247,7 +338,6 @@ bool FileEditItem::writeFile(const QString &filePath, const QString &content) file.close(); if (stream.status() != QTextStream::Ok) { - LOG_MESSAGE(QString("Error writing to file: %1").arg(filePath)); return false; } @@ -258,7 +348,6 @@ QString FileEditItem::readFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - LOG_MESSAGE(QString("Could not open file for reading: %1").arg(filePath)); return QString(); } @@ -270,184 +359,6 @@ QString FileEditItem::readFile(const QString &filePath) return content; } -QString FileEditItem::applyEditToContent(const QString &content, bool &success) -{ - success = false; - QStringList lines = content.split('\n'); - - if (m_editMode == "replace") { - QString searchPattern = m_contextBefore + m_searchText + m_contextAfter; - int pos = content.indexOf(searchPattern); - - if (pos == -1 && !m_contextBefore.isEmpty()) { - pos = content.indexOf(m_searchText); - } - - if (pos != -1) { - QString result = content; - int searchPos = result.indexOf(m_searchText, pos); - if (searchPos != -1) { - result.replace(searchPos, m_searchText.length(), m_newContent); - success = true; - return result; - } - } - - return content; - - } else if (m_editMode == "insert_before" || m_editMode == "insert_after") { - int targetLine = -1; - - if (!m_contextBefore.isEmpty() || !m_contextAfter.isEmpty()) { - for (int i = 0; i < lines.size(); ++i) { - bool contextMatches = true; - - if (!m_contextBefore.isEmpty()) { - QStringList beforeLines = m_contextBefore.split('\n'); - if (i >= beforeLines.size()) { - bool allMatch = true; - for (int j = 0; j < beforeLines.size(); ++j) { - if (lines[i - beforeLines.size() + j].trimmed() - != beforeLines[j].trimmed()) { - allMatch = false; - break; - } - } - if (!allMatch) - contextMatches = false; - } else { - contextMatches = false; - } - } - - if (contextMatches && !m_contextAfter.isEmpty()) { - QStringList afterLines = m_contextAfter.split('\n'); - if (i + afterLines.size() < lines.size()) { - bool allMatch = true; - for (int j = 0; j < afterLines.size(); ++j) { - if (lines[i + 1 + j].trimmed() != afterLines[j].trimmed()) { - allMatch = false; - break; - } - } - if (!allMatch) - contextMatches = false; - } else { - contextMatches = false; - } - } - - if (contextMatches && targetLine == -1) { - targetLine = i; - break; - } - } - } - - if (targetLine == -1 && m_lineNumber > 0 && m_lineNumber <= lines.size()) { - targetLine = m_lineNumber - 1; - } - - if (targetLine != -1) { - if (m_editMode == "insert_before") { - lines.insert(targetLine, m_newContent); - } else { - lines.insert(targetLine + 1, m_newContent); - } - success = true; - return lines.join('\n'); - } - - return content; - - } else if (m_editMode == "append") { - success = true; - return content + (content.endsWith('\n') ? "" : "\n") + m_newContent + "\n"; - } - - return content; -} - -QString FileEditItem::applyReverseEdit(const QString &content, bool &success) -{ - success = false; - QStringList lines = content.split('\n'); - - if (m_editMode == "replace") { - int pos = content.indexOf(m_newContent); - if (pos != -1) { - QString result = content; - result.replace(pos, m_newContent.length(), m_originalContent); - success = true; - return result; - } - return content; - - } else if (m_editMode == "insert_before" || m_editMode == "insert_after") { - for (int i = 0; i < lines.size(); ++i) { - if (lines[i].trimmed() == m_newContent.trimmed()) { - bool contextMatches = true; - - if (!m_contextBefore.isEmpty()) { - QStringList beforeLines = m_contextBefore.split('\n'); - if (i >= beforeLines.size()) { - for (int j = 0; j < beforeLines.size(); ++j) { - if (lines[i - beforeLines.size() + j].trimmed() != beforeLines[j].trimmed()) { - contextMatches = false; - break; - } - } - } else { - contextMatches = false; - } - } - - if (contextMatches && !m_contextAfter.isEmpty()) { - QStringList afterLines = m_contextAfter.split('\n'); - if (i + 1 + afterLines.size() <= lines.size()) { - for (int j = 0; j < afterLines.size(); ++j) { - if (lines[i + 1 + j].trimmed() != afterLines[j].trimmed()) { - contextMatches = false; - break; - } - } - } else { - contextMatches = false; - } - } - - if (contextMatches) { - lines.removeAt(i); - success = true; - return lines.join('\n'); - } - } - } - return content; - - } else if (m_editMode == "append") { - QString suffix1 = m_newContent + "\n"; - QString suffix2 = "\n" + m_newContent + "\n"; - - if (content.endsWith(suffix1)) { - QString result = content.left(content.length() - suffix1.length()); - success = true; - return result; - } else if (content.endsWith(suffix2)) { - QString result = content.left(content.length() - suffix2.length()) + "\n"; - success = true; - return result; - } else if (content.endsWith(m_newContent)) { - QString result = content.left(content.length() - m_newContent.length()); - success = true; - return result; - } - return content; - } - - return content; -} - bool FileEditItem::acquireFileLock(const QString &filePath) { QMutexLocker locker(&s_fileLockMutex); @@ -457,17 +368,13 @@ bool FileEditItem::acquireFileLock(const QString &filePath) } s_lockedFiles.insert(filePath); - LOG_MESSAGE(QString("FileEditItem: acquired lock for %1").arg(filePath)); return true; } void FileEditItem::releaseFileLock(const QString &filePath) { QMutexLocker locker(&s_fileLockMutex); - - if (s_lockedFiles.remove(filePath)) { - LOG_MESSAGE(QString("FileEditItem: released lock for %1").arg(filePath)); - } + s_lockedFiles.remove(filePath); } } // namespace QodeAssist::Chat diff --git a/ChatView/FileEditItem.hpp b/ChatView/FileEditItem.hpp index b75366b..6a0fb0e 100644 --- a/ChatView/FileEditItem.hpp +++ b/ChatView/FileEditItem.hpp @@ -47,12 +47,12 @@ public: static constexpr int MAX_RETRY_COUNT = 10; Q_PROPERTY(QString editId READ editId NOTIFY editIdChanged FINAL) + Q_PROPERTY(QString mode READ mode NOTIFY modeChanged FINAL) Q_PROPERTY(QString filePath READ filePath NOTIFY filePathChanged FINAL) - Q_PROPERTY(QString editMode READ editMode NOTIFY editModeChanged FINAL) - Q_PROPERTY(QString originalContent READ originalContent NOTIFY originalContentChanged FINAL) - Q_PROPERTY(QString newContent READ newContent NOTIFY newContentChanged FINAL) - Q_PROPERTY(QString contextBefore READ contextBefore NOTIFY contextBeforeChanged FINAL) - Q_PROPERTY(QString contextAfter READ contextAfter NOTIFY contextAfterChanged FINAL) + Q_PROPERTY(QString searchText READ searchText NOTIFY searchTextChanged FINAL) + Q_PROPERTY(QString newText READ newText NOTIFY newTextChanged FINAL) + Q_PROPERTY(QString lineBefore READ lineBefore NOTIFY lineBeforeChanged FINAL) + Q_PROPERTY(QString lineAfter READ lineAfter NOTIFY lineAfterChanged FINAL) Q_PROPERTY(int addedLines READ addedLines NOTIFY addedLinesChanged FINAL) Q_PROPERTY(int removedLines READ removedLines NOTIFY removedLinesChanged FINAL) Q_PROPERTY(EditStatus status READ status NOTIFY statusChanged FINAL) @@ -62,12 +62,12 @@ public: explicit FileEditItem(QQuickItem *parent = nullptr); QString editId() const { return m_editId; } + QString mode() const { return m_mode; } QString filePath() const { return m_filePath; } - QString editMode() const { return m_editMode; } - QString originalContent() const { return m_originalContent; } - QString newContent() const { return m_newContent; } - QString contextBefore() const { return m_contextBefore; } - QString contextAfter() const { return m_contextAfter; } + QString searchText() const { return m_searchText; } + QString newText() const { return m_newText; } + QString lineBefore() const { return m_lineBefore; } + QString lineAfter() const { return m_lineAfter; } int addedLines() const { return m_addedLines; } int removedLines() const { return m_removedLines; } EditStatus status() const { return m_status; } @@ -79,12 +79,12 @@ public: signals: void editIdChanged(); + void modeChanged(); void filePathChanged(); - void editModeChanged(); - void originalContentChanged(); - void newContentChanged(); - void contextBeforeChanged(); - void contextAfterChanged(); + void searchTextChanged(); + void newTextChanged(); + void lineBeforeChanged(); + void lineAfterChanged(); void addedLinesChanged(); void removedLinesChanged(); void statusChanged(); @@ -101,8 +101,6 @@ private: bool writeFile(const QString &filePath, const QString &content); QString readFile(const QString &filePath); - QString applyEditToContent(const QString &content, bool &success); - QString applyReverseEdit(const QString &content, bool &success); static bool acquireFileLock(const QString &filePath); static void releaseFileLock(const QString &filePath); @@ -110,14 +108,13 @@ private: static QSet s_lockedFiles; QString m_editId; + QString m_mode; QString m_filePath; - QString m_editMode; - QString m_originalContent; - QString m_newContent; - QString m_contextBefore; - QString m_contextAfter; QString m_searchText; - int m_lineNumber = -1; + QString m_newText; + QString m_lineBefore; + QString m_lineAfter; + QString m_originalContent; // Stored when applying edit int m_addedLines = 0; int m_removedLines = 0; EditStatus m_status = EditStatus::Pending; diff --git a/ChatView/qml/FileEditChangesItem.qml b/ChatView/qml/FileEditChangesItem.qml index b8c0beb..fab0a5e 100644 --- a/ChatView/qml/FileEditChangesItem.qml +++ b/ChatView/qml/FileEditChangesItem.qml @@ -153,14 +153,7 @@ FileEditItem { id: headerText Layout.fillWidth: true text: { - var modeText = "" - switch(root.editMode) { - case "replace": modeText = qsTr("Replace"); break; - case "insert_before": modeText = qsTr("Insert Before"); break; - case "insert_after": modeText = qsTr("Insert After"); break; - case "append": modeText = qsTr("Append"); break; - default: modeText = root.editMode; - } + var modeText = root.searchText.length > 0 ? qsTr("Replace") : qsTr("Append") return qsTr("%1: %2 (+%3 -%4)") .arg(modeText) .arg(root.filePath) @@ -224,21 +217,6 @@ FileEditItem { spacing: 4 visible: opacity > 0 - TextEdit { - Layout.fillWidth: true - visible: root.contextBefore.length > 0 - text: root.contextBefore - font.family: root.codeFontFamily - font.pixelSize: root.codeFontSize - color: palette.mid - wrapMode: TextEdit.Wrap - opacity: 0.6 - readOnly: true - selectByMouse: true - selectByKeyboard: true - textFormat: TextEdit.PlainText - } - Rectangle { Layout.fillWidth: true Layout.preferredHeight: oldContentText.implicitHeight + 8 @@ -246,7 +224,7 @@ FileEditItem { radius: 4 border.width: 1 border.color: Qt.rgba(1, 0.2, 0.2, 0.3) - visible: root.originalContent.length > 0 + visible: root.searchText.length > 0 TextEdit { id: oldContentText @@ -256,7 +234,7 @@ FileEditItem { top: parent.top margins: 4 } - text: "- " + root.originalContent + text: root.searchText font.family: root.codeFontFamily font.pixelSize: root.codeFontSize color: Qt.rgba(1, 0.2, 0.2, 0.9) @@ -284,7 +262,7 @@ FileEditItem { top: parent.top margins: 4 } - text: "+ " + root.newContent + text: root.newText font.family: root.codeFontFamily font.pixelSize: root.codeFontSize color: Qt.rgba(0.2, 0.8, 0.2, 0.9) @@ -296,21 +274,6 @@ FileEditItem { } } - TextEdit { - Layout.fillWidth: true - visible: root.contextAfter.length > 0 - text: root.contextAfter - font.family: root.codeFontFamily - font.pixelSize: root.codeFontSize - color: palette.mid - wrapMode: TextEdit.Wrap - opacity: 0.6 - readOnly: true - selectByMouse: true - selectByKeyboard: true - textFormat: TextEdit.PlainText - } - Text { Layout.fillWidth: true visible: root.statusMessage.length > 0 diff --git a/context/CMakeLists.txt b/context/CMakeLists.txt index 6de4dc5..28f84aa 100644 --- a/context/CMakeLists.txt +++ b/context/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(Context STATIC ProgrammingLanguage.hpp ProgrammingLanguage.cpp IContextManager.hpp IgnoreManager.hpp IgnoreManager.cpp + ProjectUtils.hpp ProjectUtils.cpp ) target_link_libraries(Context diff --git a/context/ProjectUtils.cpp b/context/ProjectUtils.cpp new file mode 100644 index 0000000..24ffcbd --- /dev/null +++ b/context/ProjectUtils.cpp @@ -0,0 +1,54 @@ + +/* + * 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 . + */ + +#include "ProjectUtils.hpp" + +#include +#include +#include + +namespace QodeAssist::Context { + +bool ProjectUtils::isFileInProject(const QString &filePath) +{ + QList projects = ProjectExplorer::ProjectManager::projects(); + Utils::FilePath targetPath = Utils::FilePath::fromString(filePath); + + 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 == targetPath) { + return true; + } + } + + Utils::FilePath projectDir = project->projectDirectory(); + if (targetPath.isChildOf(projectDir)) { + return true; + } + } + + return false; +} + +} // namespace QodeAssist::Context diff --git a/context/ProjectUtils.hpp b/context/ProjectUtils.hpp new file mode 100644 index 0000000..c250fb1 --- /dev/null +++ b/context/ProjectUtils.hpp @@ -0,0 +1,46 @@ + +/* + * 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 . + */ + +#pragma once + +#include + +namespace QodeAssist::Context { + +/** + * @brief Utility functions for working with Qt Creator projects + */ +class ProjectUtils +{ +public: + /** + * @brief Check if a file is part of any open project + * + * Checks if the given file path is either: + * 1. Explicitly listed in project source files + * 2. Located within a project directory + * + * @param filePath Absolute or canonical file path to check + * @return true if file is part of any open project, false otherwise + */ + static bool isFileInProject(const QString &filePath); +}; + +} // namespace QodeAssist::Context diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index cdc7ed6..dae3fd4 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -326,6 +326,12 @@ void LMStudioProvider::processStreamChunk(const QString &requestId, const QJsonO emit continuationStarted(requestId); LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); } + } else if ( + m_dataBuffers.contains(requestId) + && message->state() == LLMCore::MessageState::RequiresToolExecution) { + message->startNewContinuation(); + emit continuationStarted(requestId); + LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); } if (delta.contains("content") && !delta["content"].isNull()) { diff --git a/providers/LlamaCppProvider.cpp b/providers/LlamaCppProvider.cpp index b7ec209..b416cc6 100644 --- a/providers/LlamaCppProvider.cpp +++ b/providers/LlamaCppProvider.cpp @@ -330,6 +330,12 @@ void LlamaCppProvider::processStreamChunk(const QString &requestId, const QJsonO emit continuationStarted(requestId); LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); } + } else if ( + m_dataBuffers.contains(requestId) + && message->state() == LLMCore::MessageState::RequiresToolExecution) { + message->startNewContinuation(); + emit continuationStarted(requestId); + LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); } if (delta.contains("content") && !delta["content"].isNull()) { diff --git a/providers/MistralAIProvider.cpp b/providers/MistralAIProvider.cpp index f01eb6e..ad7aff6 100644 --- a/providers/MistralAIProvider.cpp +++ b/providers/MistralAIProvider.cpp @@ -347,6 +347,12 @@ void MistralAIProvider::processStreamChunk(const QString &requestId, const QJson emit continuationStarted(requestId); LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); } + } else if ( + m_dataBuffers.contains(requestId) + && message->state() == LLMCore::MessageState::RequiresToolExecution) { + message->startNewContinuation(); + emit continuationStarted(requestId); + LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); } if (delta.contains("content") && !delta["content"].isNull()) { diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index 4c5cb4e..6be7ca5 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -367,7 +367,6 @@ void OllamaProvider::onToolExecutionComplete( .arg(requestId) .arg(toolResults.size())); - message->startNewContinuation(); sendRequest(requestId, m_requestUrls[requestId], continuationRequest); } @@ -378,6 +377,17 @@ void OllamaProvider::processStreamData(const QString &requestId, const QJsonObje message = new OllamaMessage(this); m_messages[requestId] = message; LOG_MESSAGE(QString("Created NEW OllamaMessage for request %1").arg(requestId)); + + if (m_dataBuffers.contains(requestId)) { + emit continuationStarted(requestId); + LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); + } + } else if ( + m_dataBuffers.contains(requestId) + && message->state() == LLMCore::MessageState::RequiresToolExecution) { + message->startNewContinuation(); + emit continuationStarted(requestId); + LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); } if (data.contains("message")) { diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index c1599e5..6f478cc 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -304,6 +304,12 @@ void OpenAICompatProvider::processStreamChunk(const QString &requestId, const QJ emit continuationStarted(requestId); LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); } + } else if ( + m_dataBuffers.contains(requestId) + && message->state() == LLMCore::MessageState::RequiresToolExecution) { + message->startNewContinuation(); + emit continuationStarted(requestId); + LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); } if (delta.contains("content") && !delta["content"].isNull()) { diff --git a/providers/OpenAIProvider.cpp b/providers/OpenAIProvider.cpp index 0f1d04c..d945bf3 100644 --- a/providers/OpenAIProvider.cpp +++ b/providers/OpenAIProvider.cpp @@ -340,6 +340,12 @@ void OpenAIProvider::processStreamChunk(const QString &requestId, const QJsonObj emit continuationStarted(requestId); LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); } + } else if ( + m_dataBuffers.contains(requestId) + && message->state() == LLMCore::MessageState::RequiresToolExecution) { + message->startNewContinuation(); + emit continuationStarted(requestId); + LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); } if (delta.contains("content") && !delta["content"].isNull()) { diff --git a/settings/GeneralSettings.cpp b/settings/GeneralSettings.cpp index 11f0133..2583a81 100644 --- a/settings/GeneralSettings.cpp +++ b/settings/GeneralSettings.cpp @@ -226,11 +226,11 @@ GeneralSettings::GeneralSettings() Tr::tr("Allow tools to write and modify files on disk (WARNING: Use with caution!)")); allowFileSystemWrite.setDefaultValue(false); - allowReadOutsideProject.setSettingsKey(Constants::CA_ALLOW_READ_OUTSIDE_PROJECT); - allowReadOutsideProject.setLabelText(Tr::tr("Allow reading files outside project")); - allowReadOutsideProject.setToolTip( - Tr::tr("Allow tools to read files outside the project scope (system headers, Qt files, external libraries)")); - allowReadOutsideProject.setDefaultValue(true); + 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")); @@ -285,7 +285,7 @@ GeneralSettings::GeneralSettings() auto caGroup = Group{ title(TrConstants::CHAT_ASSISTANT), Column{caGrid, - Column{useTools, allowFileSystemRead, allowFileSystemWrite, allowReadOutsideProject, autoApplyFileEdits}, + Column{useTools, allowFileSystemRead, allowFileSystemWrite, allowAccessOutsideProject, autoApplyFileEdits}, caTemplateDescription}}; auto rootLayout = Column{ @@ -533,7 +533,7 @@ void GeneralSettings::resetPageToDefaults() resetAspect(useTools); resetAspect(allowFileSystemRead); resetAspect(allowFileSystemWrite); - resetAspect(allowReadOutsideProject); + resetAspect(allowAccessOutsideProject); resetAspect(autoApplyFileEdits); writeSettings(); } diff --git a/settings/GeneralSettings.hpp b/settings/GeneralSettings.hpp index 406f01a..656ff4a 100644 --- a/settings/GeneralSettings.hpp +++ b/settings/GeneralSettings.hpp @@ -103,7 +103,7 @@ public: Utils::BoolAspect useTools{this}; Utils::BoolAspect allowFileSystemRead{this}; Utils::BoolAspect allowFileSystemWrite{this}; - Utils::BoolAspect allowReadOutsideProject{this}; + Utils::BoolAspect allowAccessOutsideProject{this}; Utils::BoolAspect autoApplyFileEdits{this}; Utils::StringAspect caTemplateDescription{this}; diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 5fa7e64..0be5525 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -88,7 +88,7 @@ const char CA_ENABLE_CHAT_IN_NAVIGATION_PANEL[] = "QodeAssist.caEnableChatInNavi const char CA_USE_TOOLS[] = "QodeAssist.caUseTools"; const char CA_ALLOW_FILE_SYSTEM_READ[] = "QodeAssist.caAllowFileSystemRead"; const char CA_ALLOW_FILE_SYSTEM_WRITE[] = "QodeAssist.caAllowFileSystemWrite"; -const char CA_ALLOW_READ_OUTSIDE_PROJECT[] = "QodeAssist.caAllowReadOutsideProject"; +const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideProject"; const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions"; const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId"; diff --git a/tools/CreateNewFileTool.cpp b/tools/CreateNewFileTool.cpp index 7d4ab96..536e7b4 100644 --- a/tools/CreateNewFileTool.cpp +++ b/tools/CreateNewFileTool.cpp @@ -20,7 +20,9 @@ #include "CreateNewFileTool.hpp" #include "ToolExceptions.hpp" +#include #include +#include #include #include #include @@ -95,6 +97,21 @@ QFuture CreateNewFileTool::executeAsync(const QJsonObject &input) } QFileInfo fileInfo(filePath); + QString absolutePath = fileInfo.absoluteFilePath(); + + // Check if the file path is within the project + bool isInProject = Context::ProjectUtils::isFileInProject(absolutePath); + + if (!isInProject) { + const auto &settings = Settings::generalSettings(); + if (!settings.allowAccessOutsideProject()) { + throw ToolRuntimeError( + QString("Error: File path '%1' is not within the current project. " + "Enable 'Allow file access outside project' in settings to create files outside project scope.") + .arg(absolutePath)); + } + LOG_MESSAGE(QString("Creating file outside project scope: %1").arg(absolutePath)); + } if (fileInfo.exists()) { throw ToolRuntimeError( @@ -107,17 +124,17 @@ QFuture CreateNewFileTool::executeAsync(const QJsonObject &input) .arg(dir.absolutePath())); } - QFile file(filePath); + QFile file(absolutePath); if (!file.open(QIODevice::WriteOnly)) { throw ToolRuntimeError( - QString("Error: Could not create file '%1': %2").arg(filePath, file.errorString())); + QString("Error: Could not create file '%1': %2").arg(absolutePath, file.errorString())); } file.close(); - LOG_MESSAGE(QString("Successfully created new file: %1").arg(filePath)); + LOG_MESSAGE(QString("Successfully created new file: %1").arg(absolutePath)); - return QString("Successfully created new file: %1").arg(filePath); + return QString("Successfully created new file: %1").arg(absolutePath); }); } diff --git a/tools/EditFileTool.cpp b/tools/EditFileTool.cpp new file mode 100644 index 0000000..30d2444 --- /dev/null +++ b/tools/EditFileTool.cpp @@ -0,0 +1,284 @@ +/* + * 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 . + */ + +#include "EditFileTool.hpp" +#include "ToolExceptions.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 project file with two distinct modes of operation. " + "IMPORTANT: All text fields must contain COMPLETE LINES with trailing newlines (\\n). " + "If you forget to add \\n, it will be added automatically.\n" + "\n" + "TWO MODES OF OPERATION:\n" + "\n" + "1. REPLACE MODE (mode='replace'):\n" + " - Finds search_text in the file and replaces it with new_text\n" + " - REQUIRED: search_text (the text to find and replace)\n" + " - OPTIONAL: line_before and/or line_after for context verification\n" + " - Context verification strategy:\n" + " * line_before/line_after are searched NEAR search_text (within ~500 chars)\n" + " * They do NOT need to be immediately adjacent to search_text\n" + " * Both provided: BOTH must be found near search_text (most precise)\n" + " * Only line_before: must be found BEFORE search_text\n" + " * Only line_after: must be found AFTER search_text\n" + " * Neither: accepts first occurrence (use when search_text is unique)\n" + " - Use this to replace existing content\n" + "\n" + "2. INSERT_AFTER MODE (mode='insert_after'):\n" + " - Inserts new_text after the line specified in line_before\n" + " - If line_before is empty, inserts at the beginning of the file (useful for empty files)\n" + " - OPTIONAL: line_before (the line to insert after; empty = insert at start)\n" + " - OPTIONAL: line_after (must IMMEDIATELY follow line_before for verification)\n" + " - search_text is IGNORED\n" + " - Use this to insert content after a specific line or at the start of file\n" + " - Note: In this mode, new_text is inserted RIGHT AFTER line_before\n" + "\n" + "BEST PRACTICES for multiple edits to the same file:\n" + "- Use INSERT_AFTER mode for sequential additions (each edit uses previous addition as line_before)\n" + "- Provide stable context lines that won't be modified by current or subsequent edits\n" + "- For empty files: use INSERT_AFTER with empty line_before\n" + "- Example: When adding multiple class properties:\n" + " * First edit: INSERT_AFTER with class declaration as line_before\n" + " * Second edit: INSERT_AFTER with first property as line_before\n" + " * Third edit: INSERT_AFTER with second property as line_before\n" + "\n" + "Parameters:\n" + "- mode: 'replace' or 'insert_after'\n" + "- filepath: absolute or relative file path to edit\n" + "- new_text: complete line(s) to insert/replace (with \\n)\n" + "- search_text: (replace mode only) complete line(s) to find and replace (with \\n)\n" + "- line_before: complete line for context (with \\n), usage depends on mode\n" + "- line_after: complete line for context (with \\n), usage depends on mode"; +} + +QJsonObject EditFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const +{ + QJsonObject properties; + + QJsonObject modeProperty; + modeProperty["type"] = "string"; + modeProperty["enum"] = QJsonArray({"replace", "insert_after"}); + modeProperty["description"] = "Edit mode: 'replace' (replace search_text with new_text), " + "'insert_after' (insert new_text after line_before)"; + properties["mode"] = modeProperty; + + QJsonObject filepathProperty; + filepathProperty["type"] = "string"; + filepathProperty["description"] = "The absolute or relative file path to edit"; + properties["filepath"] = filepathProperty; + + QJsonObject newTextProperty; + newTextProperty["type"] = "string"; + newTextProperty["description"] = "Complete line(s) to insert/replace/append. Trailing newline (\\n) auto-added if missing. " + "Example: 'int main(int argc, char *argv[]) {\\n' or 'void foo();\\nvoid bar();\\n'"; + properties["new_text"] = newTextProperty; + + QJsonObject searchTextProperty; + searchTextProperty["type"] = "string"; + searchTextProperty["description"] + = "Complete line(s) to search for and replace. Trailing newline (\\n) auto-added if missing. " + "REQUIRED for 'replace' mode, IGNORED for other modes. " + "Example: 'int main() {\\n' or 'void foo();\\n'"; + properties["search_text"] = searchTextProperty; + + QJsonObject lineBeforeProperty; + lineBeforeProperty["type"] = "string"; + lineBeforeProperty["description"] = "Complete line for context verification. Trailing newline (\\n) auto-added if missing. " + "Usage depends on mode:\n" + "- 'replace': OPTIONAL, searched BEFORE search_text (within ~500 chars, not necessarily adjacent)\n" + "- 'insert_after': OPTIONAL, new_text inserted RIGHT AFTER this line. " + "If empty, inserts at the beginning of the file (useful for empty files)\n" + "Example: 'class Movie {\\n' or '#include \\n'"; + properties["line_before"] = lineBeforeProperty; + + QJsonObject lineAfterProperty; + lineAfterProperty["type"] = "string"; + lineAfterProperty["description"] = "Complete line for context verification. Trailing newline (\\n) auto-added if missing. " + "Usage depends on mode:\n" + "- 'replace': OPTIONAL, searched AFTER search_text (within ~500 chars, not necessarily adjacent)\n" + "- 'insert_after': OPTIONAL, must IMMEDIATELY follow line_before for verification\n" + "Example: '}\\n' or 'public:\\n'"; + properties["line_after"] = lineAfterProperty; + + QJsonObject definition; + definition["type"] = "object"; + definition["properties"] = properties; + + QJsonArray required; + required.append("mode"); + required.append("filepath"); + required.append("new_text"); + 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::ToolPermissions(LLMCore::ToolPermission::FileSystemRead) + | LLMCore::ToolPermissions(LLMCore::ToolPermission::FileSystemWrite); +} + +QFuture EditFileTool::executeAsync(const QJsonObject &input) +{ + return QtConcurrent::run([this, input]() -> QString { + QString mode = input["mode"].toString(); + if (mode.isEmpty()) { + throw ToolInvalidArgument("Error: mode parameter is required. Must be one of: 'replace', 'insert_after'"); + } + + if (mode != "replace" && mode != "insert_after") { + throw ToolInvalidArgument(QString("Error: invalid mode '%1'. Must be one of: 'replace', 'insert_after'").arg(mode)); + } + + QString inputFilepath = input["filepath"].toString(); + if (inputFilepath.isEmpty()) { + throw ToolInvalidArgument("Error: filepath parameter is required"); + } + + QString newText = input["new_text"].toString(); + if (newText.isEmpty()) { + throw ToolInvalidArgument("Error: new_text parameter is required"); + } + + QString searchText = input["search_text"].toString(); + QString lineBefore = input["line_before"].toString(); + QString lineAfter = input["line_after"].toString(); + + if (mode == "replace" && searchText.isEmpty()) { + throw ToolInvalidArgument("Error: search_text is required for 'replace' mode"); + } + + // Normalize text fields: ensure trailing newline if not empty + // This handles cases where LLM forgets to add \n + auto normalizeText = [](QString &text) { + if (!text.isEmpty() && !text.endsWith('\n')) { + LOG_MESSAGE(QString("EditFileTool: normalizing text, adding trailing newline (length: %1)").arg(text.length())); + text += '\n'; + } + }; + + normalizeText(newText); + if (!searchText.isEmpty()) normalizeText(searchText); + if (!lineBefore.isEmpty()) normalizeText(lineBefore); + if (!lineAfter.isEmpty()) normalizeText(lineAfter); + + QString filePath; + QFileInfo fileInfo(inputFilepath); + + if (fileInfo.isAbsolute()) { + filePath = inputFilepath; + } else { + auto projects = ProjectExplorer::ProjectManager::projects(); + if (!projects.isEmpty() && projects.first()) { + QString projectDir = projects.first()->projectDirectory().toUrlishString(); + filePath = QDir(projectDir).absoluteFilePath(inputFilepath); + } else { + filePath = QFileInfo(inputFilepath).absoluteFilePath(); + } + } + + if (!QFileInfo::exists(filePath)) { + throw ToolRuntimeError(QString("Error: File '%1' does not exist").arg(filePath)); + } + + bool isInProject = Context::ProjectUtils::isFileInProject(filePath); + + if (!isInProject) { + const auto &settings = Settings::generalSettings(); + if (!settings.allowAccessOutsideProject()) { + throw ToolRuntimeError( + QString("Error: File '%1' is outside the project scope. " + "Enable 'Allow file access outside project' in settings to edit files outside project scope.") + .arg(filePath)); + } + LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath)); + } + + auto project = isInProject ? ProjectExplorer::ProjectManager::projectForFile( + Utils::FilePath::fromString(filePath)) : nullptr; + + if (project && m_ignoreManager->shouldIgnore(filePath, project)) { + throw ToolRuntimeError( + QString("Error: File '%1' is excluded by .qodeassistignore").arg(inputFilepath)); + } + + QJsonObject result; + result["type"] = "file_edit"; + result["mode"] = mode; + result["filepath"] = filePath; + result["new_text"] = newText; + result["search_text"] = searchText; + result["line_before"] = lineBefore; + result["line_after"] = lineAfter; + + QJsonDocument doc(result); + return QString("QODEASSIST_FILE_EDIT:%1") + .arg(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); + }); +} + +} // namespace QodeAssist::Tools + diff --git a/tools/EditProjectFileTool.hpp b/tools/EditFileTool.hpp similarity index 70% rename from tools/EditProjectFileTool.hpp rename to tools/EditFileTool.hpp index f7c70d3..f25c63d 100644 --- a/tools/EditProjectFileTool.hpp +++ b/tools/EditFileTool.hpp @@ -24,11 +24,11 @@ namespace QodeAssist::Tools { -class EditProjectFileTool : public LLMCore::BaseTool +class EditFileTool : public LLMCore::BaseTool { Q_OBJECT public: - explicit EditProjectFileTool(QObject *parent = nullptr); + explicit EditFileTool(QObject *parent = nullptr); QString name() const override; QString stringName() const override; @@ -39,19 +39,6 @@ public: QFuture executeAsync(const QJsonObject &input = QJsonObject()) override; private: - enum class EditMode { Replace, InsertBefore, InsertAfter, AppendToEnd }; - - QString findFileInProject(const QString &fileName) const; - QString readFileContent(const QString &filePath) const; - void extractContext( - const QString &content, - EditMode mode, - const QString &searchText, - int lineNumber, - QString &contextBefore, - QString &contextAfter, - int contextLines = 3) const; - Context::IgnoreManager *m_ignoreManager; }; diff --git a/tools/EditProjectFileTool.cpp b/tools/EditProjectFileTool.cpp deleted file mode 100644 index 53873be..0000000 --- a/tools/EditProjectFileTool.cpp +++ /dev/null @@ -1,376 +0,0 @@ -/* - * 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 . - */ - -#include "EditProjectFileTool.hpp" -#include "ToolExceptions.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Tools { - -EditProjectFileTool::EditProjectFileTool(QObject *parent) - : BaseTool(parent) - , m_ignoreManager(new Context::IgnoreManager(this)) -{} - -QString EditProjectFileTool::name() const -{ - return "edit_project_file"; -} - -QString EditProjectFileTool::stringName() const -{ - return {"Editing project file"}; -} - -QString EditProjectFileTool::description() const -{ - return "Edit a project file using different modes: replace text, insert before/after line, or append. " - "Changes require user approval and show a diff preview. " - "Files excluded by .qodeassistignore cannot be edited. " - "Line numbers are 1-based."; -} - -QJsonObject EditProjectFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const -{ - QJsonObject properties; - - QJsonObject filenameProperty; - filenameProperty["type"] = "string"; - filenameProperty["description"] = "The filename or relative path to edit"; - properties["filename"] = filenameProperty; - - QJsonObject modeProperty; - modeProperty["type"] = "string"; - modeProperty["description"] - = "Edit mode: 'replace', 'insert_before', 'insert_after', or 'append'"; - QJsonArray modeEnum; - modeEnum.append("replace"); - modeEnum.append("insert_before"); - modeEnum.append("insert_after"); - modeEnum.append("append"); - modeProperty["enum"] = modeEnum; - properties["mode"] = modeProperty; - - QJsonObject searchTextProperty; - searchTextProperty["type"] = "string"; - searchTextProperty["description"] - = "Text to search for and replace (required for 'replace' mode)"; - properties["search_text"] = searchTextProperty; - - QJsonObject newTextProperty; - newTextProperty["type"] = "string"; - newTextProperty["description"] = "New text to insert or use as replacement"; - properties["new_text"] = newTextProperty; - - QJsonObject lineNumberProperty; - lineNumberProperty["type"] = "integer"; - lineNumberProperty["description"] - = "Line number for insert operations (1-based, required for insert modes)"; - properties["line_number"] = lineNumberProperty; - - QJsonObject definition; - definition["type"] = "object"; - definition["properties"] = properties; - - QJsonArray required; - required.append("filename"); - required.append("mode"); - required.append("new_text"); - 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 EditProjectFileTool::requiredPermissions() const -{ - return LLMCore::ToolPermissions(LLMCore::ToolPermission::FileSystemRead) - | LLMCore::ToolPermissions(LLMCore::ToolPermission::FileSystemWrite); -} - -QFuture EditProjectFileTool::executeAsync(const QJsonObject &input) -{ - return QtConcurrent::run([this, input]() -> QString { - QString filename = input["filename"].toString(); - if (filename.isEmpty()) { - QString error = "Error: filename parameter is required"; - throw ToolInvalidArgument(error); - } - - QString modeStr = input["mode"].toString(); - if (modeStr.isEmpty()) { - QString error = "Error: mode parameter is required"; - throw ToolInvalidArgument(error); - } - - EditMode mode; - if (modeStr == "replace") { - mode = EditMode::Replace; - } else if (modeStr == "insert_before") { - mode = EditMode::InsertBefore; - } else if (modeStr == "insert_after") { - mode = EditMode::InsertAfter; - } else if (modeStr == "append") { - mode = EditMode::AppendToEnd; - } else { - QString error = QString("Error: Invalid mode '%1'. Must be one of: replace, " - "insert_before, insert_after, append") - .arg(modeStr); - throw ToolInvalidArgument(error); - } - - QString newText = input["new_text"].toString(); - if (newText.isEmpty()) { - QString error = "Error: new_text parameter is required"; - throw ToolInvalidArgument(error); - } - - QString searchText = input["search_text"].toString(); - if (mode == EditMode::Replace && searchText.isEmpty()) { - QString error = "Error: search_text parameter is required for replace mode"; - throw ToolInvalidArgument(error); - } - - int lineNumber = input["line_number"].toInt(0); - if ((mode == EditMode::InsertBefore || mode == EditMode::InsertAfter) && lineNumber <= 0) { - QString error = "Error: line_number parameter is required for insert modes and must " - "be greater than 0"; - throw ToolInvalidArgument(error); - } - - QString filePath = findFileInProject(filename); - if (filePath.isEmpty()) { - QString error = QString("Error: File '%1' not found in project").arg(filename); - throw ToolRuntimeError(error); - } - - auto project = ProjectExplorer::ProjectManager::projectForFile( - Utils::FilePath::fromString(filePath)); - if (project && m_ignoreManager->shouldIgnore(filePath, project)) { - QString error - = QString("Error: File '%1' is excluded by .qodeassistignore and cannot be edited") - .arg(filename); - throw ToolRuntimeError(error); - } - - // readFileContent throws exception if file cannot be opened - QString originalContent = readFileContent(filePath); - - LOG_MESSAGE(QString("Prepared file edit: %1 (mode: %2)").arg(filePath, modeStr)); - - QString editId = QString("edit_%1_%2") - .arg(QDateTime::currentMSecsSinceEpoch()) - .arg(QRandomGenerator::global()->generate()); - - QString contextBefore, contextAfter; - extractContext(originalContent, mode, searchText, lineNumber, contextBefore, contextAfter); - - QJsonObject result; - result["type"] = "file_edit"; - result["edit_id"] = editId; - result["file_path"] = filePath; - result["mode"] = modeStr; - result["original_content"] = (mode == EditMode::Replace) ? searchText : ""; - result["new_content"] = newText; - result["context_before"] = contextBefore; - result["context_after"] = contextAfter; - result["search_text"] = searchText; - result["line_number"] = lineNumber; - - QJsonDocument doc(result); - return QString("QODEASSIST_FILE_EDIT:%1") - .arg(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); - }); -} - -QString EditProjectFileTool::findFileInProject(const QString &fileName) const -{ - QList projects = ProjectExplorer::ProjectManager::projects(); - if (projects.isEmpty()) { - LOG_MESSAGE("No projects found"); - return QString(); - } - - struct FileMatch - { - QString path; - int priority; // 1 = exact filename, 2 = ends with, 3 = contains - }; - - QVector matches; - - for (auto project : projects) { - if (!project) - continue; - - Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles); - - for (const auto &projectFile : std::as_const(projectFiles)) { - QString absolutePath = projectFile.path(); - - if (m_ignoreManager->shouldIgnore(absolutePath, project)) { - continue; - } - - QString baseName = projectFile.fileName(); - - if (baseName == fileName) { - return absolutePath; - } - - if (projectFile.endsWith(fileName)) { - matches.append({absolutePath, 2}); - } else if (baseName.contains(fileName, Qt::CaseInsensitive)) { - matches.append({absolutePath, 3}); - } - } - } - - if (!matches.isEmpty()) { - std::sort(matches.begin(), matches.end(), [](const FileMatch &a, const FileMatch &b) { - return a.priority < b.priority; - }); - return matches.first().path; - } - - return QString(); -} - -QString EditProjectFileTool::readFileContent(const QString &filePath) const -{ - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - LOG_MESSAGE(QString("Could not open file for reading: %1, error: %2") - .arg(filePath, file.errorString())); - throw ToolRuntimeError(QString("Error: Could not open file '%1': %2") - .arg(filePath, file.errorString())); - } - - QTextStream stream(&file); - stream.setAutoDetectUnicode(true); - QString content = stream.readAll(); - file.close(); - - LOG_MESSAGE(QString("Successfully read file for edit: %1, size: %2 bytes") - .arg(filePath) - .arg(content.length())); - - return content; -} - -void EditProjectFileTool::extractContext( - const QString &content, - EditMode mode, - const QString &searchText, - int lineNumber, - QString &contextBefore, - QString &contextAfter, - int contextLines) const -{ - contextBefore.clear(); - contextAfter.clear(); - - QStringList lines = content.split('\n'); - int targetLine = -1; - - if (mode == EditMode::Replace && !searchText.isEmpty()) { - int bestMatch = -1; - int maxScore = -1; - - QStringList searchLines = searchText.split('\n'); - - for (int i = 0; i < lines.size(); ++i) { - bool matches = true; - if (i + searchLines.size() > lines.size()) { - continue; - } - - for (int j = 0; j < searchLines.size(); ++j) { - if (lines[i + j] != searchLines[j]) { - matches = false; - break; - } - } - - if (matches) { - int score = 0; - for (int offset = 1; offset <= contextLines; ++offset) { - if (i - offset >= 0) score++; - if (i + searchLines.size() + offset - 1 < lines.size()) score++; - } - - if (score > maxScore) { - maxScore = score; - bestMatch = i; - } - } - } - - targetLine = bestMatch; - } else if (mode == EditMode::InsertBefore || mode == EditMode::InsertAfter) { - if (lineNumber > 0 && lineNumber <= lines.size()) { - targetLine = lineNumber - 1; - } - } else if (mode == EditMode::AppendToEnd) { - if (!lines.isEmpty()) { - int startLine = qMax(0, lines.size() - contextLines); - contextBefore = lines.mid(startLine).join('\n'); - } - return; - } - - if (targetLine == -1) { - return; - } - - int startBefore = qMax(0, targetLine - contextLines); - int countBefore = targetLine - startBefore; - contextBefore = lines.mid(startBefore, countBefore).join('\n'); - - int startAfter = targetLine + 1; - int countAfter = qMin(contextLines, lines.size() - startAfter); - contextAfter = lines.mid(startAfter, countAfter).join('\n'); -} - -} // namespace QodeAssist::Tools - diff --git a/tools/FindFileTool.cpp b/tools/FindFileTool.cpp index 623c38b..a83bfd2 100644 --- a/tools/FindFileTool.cpp +++ b/tools/FindFileTool.cpp @@ -20,6 +20,7 @@ #include "FindFileTool.hpp" #include "ToolExceptions.hpp" +#include #include #include #include @@ -132,14 +133,14 @@ QFuture FindFileTool::executeAsync(const QJsonObject &input) QFileInfo queryInfo(query); if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) { QString canonicalPath = queryInfo.canonicalFilePath(); - bool isInProject = isFileInProject(canonicalPath); + bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath); // Check if reading outside project is allowed if (!isInProject) { const auto &settings = Settings::generalSettings(); - if (!settings.allowReadOutsideProject()) { + if (!settings.allowAccessOutsideProject()) { QString error = QString("Error: File '%1' exists but is outside the project scope. " - "Enable 'Allow reading files outside project' in settings to access this file.") + "Enable 'Allow file access outside project' in settings to access files outside project scope.") .arg(canonicalPath); throw std::runtime_error(error.toStdString()); } @@ -426,29 +427,4 @@ QString FindFileTool::formatResults(const QList &matches, return result.trimmed(); } -bool FindFileTool::isFileInProject(const QString &filePath) const -{ - QList projects = ProjectExplorer::ProjectManager::projects(); - Utils::FilePath targetPath = Utils::FilePath::fromString(filePath); - - 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 == targetPath) { - return true; - } - } - - Utils::FilePath projectDir = project->projectDirectory(); - if (targetPath.isChildOf(projectDir)) { - return true; - } - } - - return false; -} - } // namespace QodeAssist::Tools diff --git a/tools/FindFileTool.hpp b/tools/FindFileTool.hpp index 0398bbb..7262e62 100644 --- a/tools/FindFileTool.hpp +++ b/tools/FindFileTool.hpp @@ -66,7 +66,6 @@ private: int ¤tDepth, int maxDepth = 10) const; QString formatResults(const QList &matches, int totalFound, int maxResults) const; - bool isFileInProject(const QString &filePath) const; bool matchesFilePattern(const QString &fileName, const QString &pattern) const; static constexpr int DEFAULT_MAX_RESULTS = 50; diff --git a/tools/ReadFilesByPathTool.cpp b/tools/ReadFilesByPathTool.cpp index e845f57..69b9e4e 100644 --- a/tools/ReadFilesByPathTool.cpp +++ b/tools/ReadFilesByPathTool.cpp @@ -20,6 +20,7 @@ #include "ReadFilesByPathTool.hpp" #include "ToolExceptions.hpp" +#include #include #include #include @@ -137,31 +138,6 @@ QFuture ReadFilesByPathTool::executeAsync(const QJsonObject &input) }); } -bool ReadFilesByPathTool::isFileInProject(const QString &filePath) const -{ - QList projects = ProjectExplorer::ProjectManager::projects(); - Utils::FilePath targetPath = Utils::FilePath::fromString(filePath); - - 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 == targetPath) { - return true; - } - } - - Utils::FilePath projectDir = project->projectDirectory(); - if (targetPath.isChildOf(projectDir)) { - return true; - } - } - - return false; -} - QString ReadFilesByPathTool::readFileContent(const QString &filePath) const { QFile file(filePath); @@ -206,15 +182,15 @@ ReadFilesByPathTool::FileResult ReadFilesByPathTool::processFile(const QString & QString canonicalPath = fileInfo.canonicalFilePath(); LOG_MESSAGE(QString("Canonical path: %1").arg(canonicalPath)); - bool isInProject = isFileInProject(canonicalPath); + bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath); if (!isInProject) { const auto &settings = Settings::generalSettings(); - if (!settings.allowReadOutsideProject()) { + if (!settings.allowAccessOutsideProject()) { result.error = QString( "File is not part of the project. " - "Enable 'Allow reading files outside project' in settings " - "to access this file."); + "Enable 'Allow file access outside project' in settings " + "to read files outside project scope."); return result; } LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath)); diff --git a/tools/ReadFilesByPathTool.hpp b/tools/ReadFilesByPathTool.hpp index 5e751bc..2ce8673 100644 --- a/tools/ReadFilesByPathTool.hpp +++ b/tools/ReadFilesByPathTool.hpp @@ -48,7 +48,6 @@ private: }; QString readFileContent(const QString &filePath) const; - bool isFileInProject(const QString &filePath) const; FileResult processFile(const QString &filePath) const; QString formatResults(const QList &results) const; Context::IgnoreManager *m_ignoreManager; diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp index 7db04ef..90eb80f 100644 --- a/tools/ToolsFactory.cpp +++ b/tools/ToolsFactory.cpp @@ -25,7 +25,7 @@ #include #include "CreateNewFileTool.hpp" -#include "EditProjectFileTool.hpp" +#include "EditFileTool.hpp" #include "FindFileTool.hpp" #include "FindSymbolTool.hpp" #include "GetIssuesListTool.hpp" @@ -49,7 +49,7 @@ void ToolsFactory::registerTools() registerTool(new ListProjectFilesTool(this)); registerTool(new SearchInProjectTool(this)); registerTool(new GetIssuesListTool(this)); - registerTool(new EditProjectFileTool(this)); + registerTool(new EditFileTool(this)); registerTool(new FindSymbolTool(this)); registerTool(new FindFileTool(this)); registerTool(new CreateNewFileTool(this)); diff --git a/tools/ToolsManager.cpp b/tools/ToolsManager.cpp index 09c5486..2729013 100644 --- a/tools/ToolsManager.cpp +++ b/tools/ToolsManager.cpp @@ -50,29 +50,85 @@ void ToolsManager::executeToolCall( const QString &toolName, const QJsonObject &input) { - LOG_MESSAGE(QString("ToolsManager: Executing tool %1 (ID: %2) for request %3") + LOG_MESSAGE(QString("ToolsManager: Queueing tool %1 (ID: %2) for request %3") .arg(toolName, toolId, requestId)); - if (!m_pendingTools.contains(requestId)) { - m_pendingTools[requestId] = QHash(); + if (!m_toolQueues.contains(requestId)) { + m_toolQueues[requestId] = ToolQueue(); } - if (m_pendingTools[requestId].contains(toolId)) { - LOG_MESSAGE(QString("Tool %1 already in progress for request %2").arg(toolId, requestId)); - return; - } - - auto tool = m_toolsFactory->getToolByName(toolName); - if (!tool) { - LOG_MESSAGE(QString("ToolsManager: Tool not found: %1").arg(toolName)); + auto &queue = m_toolQueues[requestId]; + + // Check if tool already exists in queue or completed + for (const auto &tool : queue.queue) { + if (tool.id == toolId) { + LOG_MESSAGE(QString("Tool %1 already in queue for request %2").arg(toolId, requestId)); + return; + } + } + + if (queue.completed.contains(toolId)) { + LOG_MESSAGE( + QString("Tool %1 already completed for request %2").arg(toolId, requestId)); return; } + // Add tool to queue PendingTool pendingTool{toolId, toolName, input, "", false}; - m_pendingTools[requestId][toolId] = pendingTool; + queue.queue.append(pendingTool); - m_toolHandler->executeToolAsync(requestId, toolId, tool, input); - LOG_MESSAGE(QString("ToolsManager: Started async execution of %1").arg(toolName)); + LOG_MESSAGE(QString("ToolsManager: Tool %1 added to queue (position %2)") + .arg(toolName) + .arg(queue.queue.size())); + + // Start execution if not already running + if (!queue.isExecuting) { + executeNextTool(requestId); + } +} + +void ToolsManager::executeNextTool(const QString &requestId) +{ + if (!m_toolQueues.contains(requestId)) { + return; + } + + auto &queue = m_toolQueues[requestId]; + + // Check if queue is empty + if (queue.queue.isEmpty()) { + LOG_MESSAGE(QString("ToolsManager: All tools complete for request %1, emitting results") + .arg(requestId)); + QHash results = getToolResults(requestId); + emit toolExecutionComplete(requestId, results); + queue.isExecuting = false; + return; + } + + // Get next tool from queue + PendingTool tool = queue.queue.takeFirst(); + queue.isExecuting = true; + + LOG_MESSAGE(QString("ToolsManager: Executing tool %1 (ID: %2) for request %3 (%4 remaining)") + .arg(tool.name, tool.id, requestId) + .arg(queue.queue.size())); + + auto toolInstance = m_toolsFactory->getToolByName(tool.name); + if (!toolInstance) { + 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.complete = true; + queue.completed[tool.id] = tool; + executeNextTool(requestId); + return; + } + + // Store tool in completed map (will be updated when finished) + queue.completed[tool.id] = tool; + + m_toolHandler->executeToolAsync(requestId, tool.id, toolInstance, tool.input); + LOG_MESSAGE(QString("ToolsManager: Started async execution of %1").arg(tool.name)); } QJsonArray ToolsManager::getToolsDefinitions(LLMCore::ToolSchemaFormat format) const @@ -86,10 +142,10 @@ QJsonArray ToolsManager::getToolsDefinitions(LLMCore::ToolSchemaFormat format) c void ToolsManager::cleanupRequest(const QString &requestId) { - if (m_pendingTools.contains(requestId)) { + if (m_toolQueues.contains(requestId)) { LOG_MESSAGE(QString("ToolsManager: Canceling pending tools for request %1").arg(requestId)); m_toolHandler->cleanupRequest(requestId); - m_pendingTools.remove(requestId); + m_toolQueues.remove(requestId); } LOG_MESSAGE(QString("ToolsManager: Cleaned up request %1").arg(requestId)); @@ -98,13 +154,20 @@ void ToolsManager::cleanupRequest(const QString &requestId) void ToolsManager::onToolFinished( const QString &requestId, const QString &toolId, const QString &result, bool success) { - if (!m_pendingTools.contains(requestId) || !m_pendingTools[requestId].contains(toolId)) { + if (!m_toolQueues.contains(requestId)) { + LOG_MESSAGE(QString("ToolsManager: Tool result for unknown request %1").arg(requestId)); + return; + } + + auto &queue = m_toolQueues[requestId]; + + if (!queue.completed.contains(toolId)) { LOG_MESSAGE(QString("ToolsManager: Tool result for unknown tool %1 in request %2") .arg(toolId, requestId)); return; } - PendingTool &tool = m_pendingTools[requestId][toolId]; + PendingTool &tool = queue.completed[toolId]; tool.result = success ? result : QString("Error: %1").arg(result); tool.complete = true; @@ -113,14 +176,8 @@ void ToolsManager::onToolFinished( .arg(success ? QString("completed") : QString("failed")) .arg(requestId)); - if (isExecutionComplete(requestId)) { - QHash results = getToolResults(requestId); - LOG_MESSAGE(QString("ToolsManager: All tools complete for request %1, emitting results") - .arg(requestId)); - emit toolExecutionComplete(requestId, results); - } else { - LOG_MESSAGE(QString("ToolsManager: Tools still pending for request %1").arg(requestId)); - } + // Execute next tool in queue + executeNextTool(requestId); } ToolsFactory *ToolsManager::toolsFactory() const @@ -128,29 +185,13 @@ ToolsFactory *ToolsManager::toolsFactory() const return m_toolsFactory; } -bool ToolsManager::isExecutionComplete(const QString &requestId) const -{ - if (!m_pendingTools.contains(requestId)) { - return true; - } - - const auto &tools = m_pendingTools[requestId]; - for (auto it = tools.begin(); it != tools.end(); ++it) { - if (!it.value().complete) { - return false; - } - } - - return true; -} - QHash ToolsManager::getToolResults(const QString &requestId) const { QHash results; - if (m_pendingTools.contains(requestId)) { - const auto &tools = m_pendingTools[requestId]; - for (auto it = tools.begin(); it != tools.end(); ++it) { + if (m_toolQueues.contains(requestId)) { + const auto &queue = m_toolQueues[requestId]; + for (auto it = queue.completed.begin(); it != queue.completed.end(); ++it) { if (it.value().complete) { results[it.key()] = it.value().result; } diff --git a/tools/ToolsManager.hpp b/tools/ToolsManager.hpp index e0a13db..c94c425 100644 --- a/tools/ToolsManager.hpp +++ b/tools/ToolsManager.hpp @@ -39,6 +39,13 @@ struct PendingTool bool complete = false; }; +struct ToolQueue +{ + QList queue; + QHash completed; + bool isExecuting = false; +}; + class ToolsManager : public QObject { Q_OBJECT @@ -67,9 +74,9 @@ private slots: private: ToolsFactory *m_toolsFactory; ToolHandler *m_toolHandler; - QHash> m_pendingTools; + QHash m_toolQueues; - bool isExecutionComplete(const QString &requestId) const; + void executeNextTool(const QString &requestId); QHash getToolResults(const QString &requestId) const; };