feat: Add edit file tool (#249)

* feat: Add edit file tool
* feat: Add icons for action buttons
This commit is contained in:
Petr Mironychev
2025-11-03 08:56:52 +01:00
committed by GitHub
parent e7110810f8
commit 9b90aaa06e
39 changed files with 3732 additions and 344 deletions

View File

@ -49,7 +49,7 @@ QString BuildProjectTool::stringName() const
QString BuildProjectTool::description() const
{
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).";
}

View File

@ -23,6 +23,7 @@
#include <context/ProjectUtils.hpp>
#include <logger/Logger.hpp>
#include <settings/GeneralSettings.hpp>
#include <settings/ToolsSettings.hpp>
#include <QDir>
#include <QFile>
#include <QFileInfo>
@ -103,7 +104,7 @@ QFuture<QString> CreateNewFileTool::executeAsync(const QJsonObject &input)
bool isInProject = Context::ProjectUtils::isFileInProject(absolutePath);
if (!isInProject) {
const auto &settings = Settings::generalSettings();
const auto &settings = Settings::toolsSettings();
if (!settings.allowAccessOutsideProject()) {
throw ToolRuntimeError(
QString("Error: File path '%1' is not within the current project. "

215
tools/EditFileTool.cpp Normal file
View 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
View 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
View 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 &currentDepth,
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
View 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 &currentDepth,
int maxDepth = 5,
Context::IgnoreManager *ignoreManager = nullptr);
};
} // namespace QodeAssist::Tools

View File

@ -20,17 +20,9 @@
#include "FindAndReadFileTool.hpp"
#include "ToolExceptions.hpp"
#include <context/ProjectUtils.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 <QJsonObject>
#include <QTextStream>
#include <QtConcurrent>
namespace QodeAssist::Tools {
@ -109,14 +101,15 @@ QFuture<QString> FindAndReadFileTool::executeAsync(const QJsonObject &input)
.arg(query, filePattern.isEmpty() ? "none" : filePattern)
.arg(readContent));
FileMatch bestMatch = findBestMatch(query, filePattern, 10);
FileSearchUtils::FileMatch bestMatch = FileSearchUtils::findBestMatch(
query, filePattern, 10, m_ignoreManager);
if (bestMatch.absolutePath.isEmpty()) {
return QString("No file found matching '%1'").arg(query);
}
if (readContent) {
bestMatch.content = readFileContent(bestMatch.absolutePath);
bestMatch.content = FileSearchUtils::readFileContent(bestMatch.absolutePath);
if (bestMatch.content.isNull()) {
bestMatch.error = "Could not read file";
}
@ -126,221 +119,8 @@ QFuture<QString> FindAndReadFileTool::executeAsync(const QJsonObject &input)
});
}
FindAndReadFileTool::FileMatch FindAndReadFileTool::findBestMatch(
const QString &query, const QString &filePattern, int maxResults)
{
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 &currentDepth,
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 FindAndReadFileTool::formatResult(const FileSearchUtils::FileMatch &match,
bool readContent) const
{
QString result
= QString("Found file: %1\nAbsolute path: %2").arg(match.relativePath, match.absolutePath);

View File

@ -19,6 +19,8 @@
#pragma once
#include "FileSearchUtils.hpp"
#include <context/IgnoreManager.hpp>
#include <llmcore/BaseTool.hpp>
#include <QFuture>
@ -42,38 +44,7 @@ public:
QFuture<QString> executeAsync(const QJsonObject &input) override;
private:
enum class MatchType { ExactName, PathMatch, PartialName };
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 &currentDepth,
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;
QString formatResult(const FileSearchUtils::FileMatch &match, bool readContent) const;
Context::IgnoreManager *m_ignoreManager;
};

View File

@ -21,11 +21,13 @@
#include "logger/Logger.hpp"
#include <settings/GeneralSettings.hpp>
#include <settings/ToolsSettings.hpp>
#include <QJsonArray>
#include <QJsonObject>
#include "BuildProjectTool.hpp"
#include "CreateNewFileTool.hpp"
#include "EditFileTool.hpp"
#include "FindAndReadFileTool.hpp"
#include "GetIssuesListTool.hpp"
#include "ListProjectFilesTool.hpp"
@ -46,6 +48,7 @@ void ToolsFactory::registerTools()
registerTool(new ListProjectFilesTool(this));
registerTool(new GetIssuesListTool(this));
registerTool(new CreateNewFileTool(this));
registerTool(new EditFileTool(this));
registerTool(new BuildProjectTool(this));
registerTool(new ProjectSearchTool(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 toolsArray;
const auto &settings = Settings::generalSettings();
const auto &settings = Settings::toolsSettings();
for (auto it = m_tools.constBegin(); it != m_tools.constEnd(); ++it) {
if (!it.value()) {
continue;
}
if (it.value()->name() == "edit_file" && !settings.enableEditFileTool()) {
continue;
}
const auto requiredPerms = it.value()->requiredPermissions();
bool hasPermission = true;

View File

@ -59,7 +59,6 @@ void ToolsManager::executeToolCall(
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));
@ -73,15 +72,16 @@ void ToolsManager::executeToolCall(
return;
}
// Add tool to queue
PendingTool pendingTool{toolId, toolName, input, "", false};
QJsonObject modifiedInput = input;
modifiedInput["_request_id"] = requestId;
PendingTool pendingTool{toolId, toolName, modifiedInput, "", false};
queue.queue.append(pendingTool);
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);
}
@ -95,7 +95,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
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));
@ -105,7 +104,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
return;
}
// Get next tool from queue
PendingTool tool = queue.queue.takeFirst();
queue.isExecuting = true;
@ -116,7 +114,6 @@ void ToolsManager::executeNextTool(const QString &requestId)
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;
@ -124,7 +121,6 @@ void ToolsManager::executeNextTool(const QString &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);
@ -176,7 +172,6 @@ void ToolsManager::onToolFinished(
.arg(success ? QString("completed") : QString("failed"))
.arg(requestId));
// Execute next tool in queue
executeNextTool(requestId);
}