mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-20 10:10:56 -04:00
refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore
This commit is contained in:
305
sources/tools/BuildProjectTool.cpp
Normal file
305
sources/tools/BuildProjectTool.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "BuildProjectTool.hpp"
|
||||
|
||||
#include "GetIssuesListTool.hpp"
|
||||
|
||||
#include "plugin/Version.hpp"
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/buildmanager.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectexplorerconstants.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <projectexplorer/runconfiguration.h>
|
||||
#include <projectexplorer/target.h>
|
||||
#include <projectexplorer/task.h>
|
||||
#include <utils/id.h>
|
||||
#include <QApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMetaObject>
|
||||
#include <QTimer>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
BuildProjectTool::BuildProjectTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{
|
||||
}
|
||||
|
||||
BuildProjectTool::~BuildProjectTool()
|
||||
{
|
||||
for (auto it = m_activeBuilds.begin(); it != m_activeBuilds.end(); ++it) {
|
||||
BuildInfo &info = it.value();
|
||||
if (info.buildFinishedConnection) {
|
||||
disconnect(info.buildFinishedConnection);
|
||||
}
|
||||
if (info.promise) {
|
||||
info.promise->finish();
|
||||
}
|
||||
}
|
||||
m_activeBuilds.clear();
|
||||
}
|
||||
|
||||
QString BuildProjectTool::id() const
|
||||
{
|
||||
return "build_project";
|
||||
}
|
||||
|
||||
QString BuildProjectTool::displayName() const
|
||||
{
|
||||
return "Building and running project";
|
||||
}
|
||||
|
||||
QString BuildProjectTool::description() const
|
||||
{
|
||||
return "Build the active Qt Creator project using its current build configuration and block "
|
||||
"until the build finishes. Returns success/failure along with the full compiler output "
|
||||
"(stdout + stderr). Use `get_issues_list` afterwards to get structured diagnostics. "
|
||||
"This call is blocking and may take a long time for large projects.";
|
||||
}
|
||||
|
||||
QJsonObject BuildProjectTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
|
||||
QJsonObject properties;
|
||||
properties["rebuild"] = QJsonObject{
|
||||
{"type", "boolean"},
|
||||
{"description", "Force a clean rebuild instead of incremental build (default: false)"}};
|
||||
properties["run_after_build"] = QJsonObject{
|
||||
{"type", "boolean"},
|
||||
{"description", "Run the project after successful build (default: false)"}};
|
||||
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray();
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> BuildProjectTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (!project) {
|
||||
return QtFuture::makeReadyFuture(
|
||||
LLMQore::ToolResult::error("Error: No active project found. Please open a project in Qt Creator."));
|
||||
}
|
||||
|
||||
if (ProjectExplorer::BuildManager::isBuilding(project)) {
|
||||
return QtFuture::makeReadyFuture(
|
||||
LLMQore::ToolResult::error("Error: Build is already in progress. Please wait for it to complete."));
|
||||
}
|
||||
|
||||
if (m_activeBuilds.contains(project)) {
|
||||
return QtFuture::makeReadyFuture(
|
||||
LLMQore::ToolResult::error(QString("Error: Build is already being tracked for project '%1'.")
|
||||
.arg(project->displayName())));
|
||||
}
|
||||
|
||||
bool rebuild = input.value("rebuild").toBool(false);
|
||||
bool runAfterBuild = input.value("run_after_build").toBool(false);
|
||||
|
||||
LOG_MESSAGE(QString("BuildProjectTool: %1 project '%2'%3")
|
||||
.arg(rebuild ? QString("Rebuilding") : QString("Building"))
|
||||
.arg(project->displayName())
|
||||
.arg(runAfterBuild ? QString(" (run after build)") : QString()));
|
||||
|
||||
auto promise = QSharedPointer<QPromise<LLMQore::ToolResult>>::create();
|
||||
promise->start();
|
||||
|
||||
BuildInfo buildInfo;
|
||||
buildInfo.promise = promise;
|
||||
buildInfo.project = project;
|
||||
buildInfo.projectName = project->displayName();
|
||||
buildInfo.isRebuild = rebuild;
|
||||
buildInfo.runAfterBuild = runAfterBuild;
|
||||
|
||||
auto *buildManager = ProjectExplorer::BuildManager::instance();
|
||||
buildInfo.buildFinishedConnection = QObject::connect(
|
||||
buildManager,
|
||||
&ProjectExplorer::BuildManager::buildQueueFinished,
|
||||
this,
|
||||
&BuildProjectTool::onBuildQueueFinished);
|
||||
|
||||
m_activeBuilds.insert(project, buildInfo);
|
||||
|
||||
QMetaObject::invokeMethod(
|
||||
qApp,
|
||||
[project, rebuild]() {
|
||||
if (rebuild) {
|
||||
ProjectExplorer::BuildManager::rebuildProjectWithDependencies(
|
||||
project, ProjectExplorer::ConfigSelection::Active);
|
||||
} else {
|
||||
ProjectExplorer::BuildManager::buildProjectWithDependencies(
|
||||
project, ProjectExplorer::ConfigSelection::Active);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
void BuildProjectTool::onBuildQueueFinished(bool success)
|
||||
{
|
||||
QList<ProjectExplorer::Project *> projectsToCleanup;
|
||||
|
||||
for (auto it = m_activeBuilds.begin(); it != m_activeBuilds.end(); ++it) {
|
||||
ProjectExplorer::Project *project = it.key();
|
||||
|
||||
if (!ProjectExplorer::BuildManager::isBuilding(project)) {
|
||||
BuildInfo &info = it.value();
|
||||
|
||||
if (info.promise && info.promise->future().isCanceled()) {
|
||||
LOG_MESSAGE(QString("BuildProjectTool: Build cancelled for project '%1'")
|
||||
.arg(info.projectName));
|
||||
projectsToCleanup.append(project);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString result = collectBuildResults(success, info.projectName, info.isRebuild);
|
||||
|
||||
if (success && info.runAfterBuild) {
|
||||
scheduleProjectRun(project, info.projectName, result);
|
||||
} else if (!success && info.runAfterBuild) {
|
||||
result += QString("\n\nProject was not started due to build failure.");
|
||||
}
|
||||
|
||||
if (info.promise) {
|
||||
info.promise->addResult(LLMQore::ToolResult::text(result));
|
||||
info.promise->finish();
|
||||
}
|
||||
|
||||
projectsToCleanup.append(project);
|
||||
}
|
||||
}
|
||||
|
||||
for (ProjectExplorer::Project *project : projectsToCleanup) {
|
||||
cleanupBuildInfo(project);
|
||||
}
|
||||
}
|
||||
|
||||
void BuildProjectTool::scheduleProjectRun(ProjectExplorer::Project *project,
|
||||
const QString &projectName,
|
||||
QString &result)
|
||||
{
|
||||
auto *target = project->activeTarget();
|
||||
if (!target) {
|
||||
result += QString("\n\nError: No active target found for the project.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto *runConfig = target->activeRunConfiguration();
|
||||
if (!runConfig) {
|
||||
result += QString("\n\nError: No active run configuration found for the project.");
|
||||
return;
|
||||
}
|
||||
|
||||
QString runConfigName = runConfig->displayName();
|
||||
result += QString("\n\nProject '%1' will be started with run configuration '%2'.")
|
||||
.arg(projectName, runConfigName);
|
||||
|
||||
ProjectExplorer::ProjectExplorerPlugin::runProject(project, Utils::Id(ProjectExplorer::Constants::NORMAL_RUN_MODE));
|
||||
}
|
||||
|
||||
QString BuildProjectTool::collectBuildResults(
|
||||
bool success, const QString &projectName, bool isRebuild)
|
||||
{
|
||||
QStringList results;
|
||||
|
||||
// Build header
|
||||
QString buildType = isRebuild ? QString("Rebuild") : QString("Build");
|
||||
QString statusText = success ? QString("✓ SUCCEEDED") : QString("✗ FAILED");
|
||||
|
||||
results.append(QString("%1 %2 for project '%3'\n")
|
||||
.arg(buildType, statusText, projectName));
|
||||
|
||||
const auto tasks = IssuesTracker::instance().getTasks();
|
||||
|
||||
if (!tasks.isEmpty()) {
|
||||
int errorCount = 0;
|
||||
int warningCount = 0;
|
||||
QStringList issuesList;
|
||||
|
||||
for (const ProjectExplorer::Task &task : tasks) {
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0)
|
||||
auto taskType = task.type();
|
||||
auto taskFile = task.file();
|
||||
auto taskLine = task.line();
|
||||
auto taskColumn = task.column();
|
||||
#else
|
||||
auto taskType = task.type;
|
||||
auto taskFile = task.file;
|
||||
auto taskLine = task.line;
|
||||
auto taskColumn = task.column;
|
||||
#endif
|
||||
|
||||
QString typeStr;
|
||||
switch (taskType) {
|
||||
case ProjectExplorer::Task::Error:
|
||||
typeStr = QString("ERROR");
|
||||
errorCount++;
|
||||
break;
|
||||
case ProjectExplorer::Task::Warning:
|
||||
typeStr = QString("WARNING");
|
||||
warningCount++;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (issuesList.size() < 50) {
|
||||
QString issueText = QString("[%1] %2").arg(typeStr, task.description());
|
||||
|
||||
if (!taskFile.isEmpty()) {
|
||||
issueText += QString("\n File: %1").arg(taskFile.toUrlishString());
|
||||
if (taskLine > 0) {
|
||||
issueText += QString(":%1").arg(taskLine);
|
||||
if (taskColumn > 0) {
|
||||
issueText += QString(":%1").arg(taskColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
issuesList.append(issueText);
|
||||
}
|
||||
}
|
||||
|
||||
results.append(QString("Issues found: %1 error(s), %2 warning(s)")
|
||||
.arg(errorCount)
|
||||
.arg(warningCount));
|
||||
|
||||
if (!issuesList.isEmpty()) {
|
||||
results.append("\nDetails:");
|
||||
results.append(issuesList.join("\n\n"));
|
||||
|
||||
if (errorCount + warningCount > 50) {
|
||||
results.append(
|
||||
QString("\n... and %1 more issue(s). Use get_issues_list tool for full list.")
|
||||
.arg(errorCount + warningCount - 50));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results.append("No compilation errors or warnings.");
|
||||
}
|
||||
|
||||
return results.join("\n");
|
||||
}
|
||||
|
||||
void BuildProjectTool::cleanupBuildInfo(ProjectExplorer::Project *project)
|
||||
{
|
||||
if (!m_activeBuilds.contains(project)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BuildInfo info = m_activeBuilds.take(project);
|
||||
|
||||
if (info.buildFinishedConnection) {
|
||||
disconnect(info.buildFinishedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
57
sources/tools/BuildProjectTool.hpp
Normal file
57
sources/tools/BuildProjectTool.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QPromise>
|
||||
#include <QSharedPointer>
|
||||
|
||||
namespace ProjectExplorer {
|
||||
class Project;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
struct BuildInfo
|
||||
{
|
||||
QSharedPointer<QPromise<LLMQore::ToolResult>> promise;
|
||||
QPointer<ProjectExplorer::Project> project;
|
||||
QString projectName;
|
||||
bool isRebuild = false;
|
||||
bool runAfterBuild = false;
|
||||
QMetaObject::Connection buildFinishedConnection;
|
||||
};
|
||||
|
||||
class BuildProjectTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BuildProjectTool(QObject *parent = nullptr);
|
||||
~BuildProjectTool() override;
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
private slots:
|
||||
void onBuildQueueFinished(bool success);
|
||||
|
||||
private:
|
||||
void scheduleProjectRun(ProjectExplorer::Project *project,
|
||||
const QString &projectName,
|
||||
QString &result);
|
||||
QString collectBuildResults(bool success, const QString &projectName, bool isRebuild);
|
||||
void cleanupBuildInfo(ProjectExplorer::Project *project);
|
||||
|
||||
QHash<ProjectExplorer::Project *, BuildInfo> m_activeBuilds;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
128
sources/tools/CreateNewFileTool.cpp
Normal file
128
sources/tools/CreateNewFileTool.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "CreateNewFileTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonObject>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
CreateNewFileTool::CreateNewFileTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{}
|
||||
|
||||
QString CreateNewFileTool::id() const
|
||||
{
|
||||
return "create_new_file";
|
||||
}
|
||||
|
||||
QString CreateNewFileTool::displayName() const
|
||||
{
|
||||
return {"Creating new file"};
|
||||
}
|
||||
|
||||
QString CreateNewFileTool::description() const
|
||||
{
|
||||
return "Create a new empty file at the given absolute path. Any missing parent directories "
|
||||
"are created automatically. The file is written to disk only — it is NOT added to the "
|
||||
"project's build system automatically; the user must register it in CMakeLists.txt or "
|
||||
"the equivalent project file. Use `edit_file` afterwards to populate its content.";
|
||||
}
|
||||
|
||||
QJsonObject CreateNewFileTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
QJsonObject filepathProperty;
|
||||
filepathProperty["type"] = "string";
|
||||
filepathProperty["description"]
|
||||
= "Absolute path where the new file should be created. Parent directories are made if "
|
||||
"missing. Relative paths are rejected.";
|
||||
properties["filepath"] = filepathProperty;
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
QJsonArray required;
|
||||
required.append("filepath");
|
||||
definition["required"] = required;
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> CreateNewFileTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> LLMQore::ToolResult {
|
||||
QString filePath = input["filepath"].toString();
|
||||
|
||||
if (filePath.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument("Error: 'filepath' parameter is required");
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(filePath);
|
||||
QString absolutePath = fileInfo.absoluteFilePath();
|
||||
|
||||
bool isInProject = Context::ProjectUtils::isFileInProject(absolutePath);
|
||||
|
||||
if (!isInProject) {
|
||||
const auto &settings = Settings::toolsSettings();
|
||||
if (!settings.allowAccessOutsideProject()) {
|
||||
const QString projectRoot = Context::ProjectUtils::getProjectRoot();
|
||||
const QString hint = projectRoot.isEmpty()
|
||||
? QStringLiteral(
|
||||
"No project is currently open. Open a project in Qt Creator or "
|
||||
"enable 'Allow file access outside project' in QodeAssist settings.")
|
||||
: QString(
|
||||
"Retry with a path under the active project root: '%1'. The build "
|
||||
"directory is for compiler output only and cannot accept new source "
|
||||
"files. If you really need to write outside the project, enable "
|
||||
"'Allow file access outside project' in QodeAssist settings.")
|
||||
.arg(projectRoot);
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Error: File path '%1' is not within the current project. %2")
|
||||
.arg(absolutePath, hint));
|
||||
}
|
||||
LOG_MESSAGE(QString("Creating file outside project scope: %1").arg(absolutePath));
|
||||
}
|
||||
|
||||
if (fileInfo.exists()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Error: File already exists at path '%1'").arg(filePath));
|
||||
}
|
||||
|
||||
QDir dir = fileInfo.absoluteDir();
|
||||
if (!dir.exists()) {
|
||||
if (!dir.mkpath(".")) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Error: Could not create directory: '%1'").arg(dir.absolutePath()));
|
||||
}
|
||||
LOG_MESSAGE(QString("Created directory path: %1").arg(dir.absolutePath()));
|
||||
}
|
||||
|
||||
QFile file(absolutePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Error: Could not create file '%1': %2").arg(absolutePath, file.errorString()));
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
||||
LOG_MESSAGE(QString("Successfully created new file: %1").arg(absolutePath));
|
||||
|
||||
return LLMQore::ToolResult::text(QString("Successfully created new file: %1").arg(absolutePath));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
26
sources/tools/CreateNewFileTool.hpp
Normal file
26
sources/tools/CreateNewFileTool.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class CreateNewFileTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CreateNewFileTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
224
sources/tools/EditFileTool.cpp
Normal file
224
sources/tools/EditFileTool.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "EditFileTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <context/ChangesManager.h>
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
EditFileTool::EditFileTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{}
|
||||
|
||||
QString EditFileTool::id() const
|
||||
{
|
||||
return "edit_file";
|
||||
}
|
||||
|
||||
QString EditFileTool::displayName() const
|
||||
{
|
||||
return {"Editing file"};
|
||||
}
|
||||
|
||||
QString EditFileTool::description() const
|
||||
{
|
||||
return "Edit a file by replacing old content with new content. "
|
||||
"Provide the file path (absolute or relative to project root), old_content to find and replace, "
|
||||
"and new_content to replace it with. Changes are applied immediately if auto-apply "
|
||||
"is enabled in settings. The user can undo or reapply changes at any time. "
|
||||
"\n\nIMPORTANT:"
|
||||
"\n- ALWAYS use read_file to get current file content before editing to ensure accuracy."
|
||||
"\n- Path can be absolute (e.g., /path/to/file.cpp) or relative to project root (e.g., src/main.cpp)."
|
||||
"\n- For EMPTY files: use empty old_content (empty string or omit parameter)."
|
||||
"\n- To append at the END of file: use empty old_content."
|
||||
"\n- To insert at the BEGINNING of a file (e.g., copyright header), you MUST provide "
|
||||
"the EXACT first few lines of the file as old_content (at least 3-5 lines), "
|
||||
"then put those lines + new header in new_content."
|
||||
"\n- For replacements in the middle, provide EXACT matching text with sufficient "
|
||||
"context (at least 5-10 lines) to ensure correct placement."
|
||||
"\n- The system uses fuzzy matching with 85% similarity threshold for first-time edits. "
|
||||
"Provide accurate old_content to avoid incorrect placement."
|
||||
"\n- If changes remain 'pending' and file content hasn't changed, the user likely "
|
||||
"disabled auto-apply. DO NOT retry the same edit - wait for user action.";
|
||||
}
|
||||
|
||||
QJsonObject EditFileTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
QJsonObject filenameProperty;
|
||||
filenameProperty["type"] = "string";
|
||||
filenameProperty["description"]
|
||||
= "The path of the file to edit. Can be an absolute path (e.g., /path/to/file.cpp) "
|
||||
"or a relative path from the project root (e.g., src/main.cpp)";
|
||||
properties["filename"] = filenameProperty;
|
||||
|
||||
QJsonObject oldContentProperty;
|
||||
oldContentProperty["type"] = "string";
|
||||
oldContentProperty["description"]
|
||||
= "The content to find and replace. For exact matches, provide precise text "
|
||||
"(including whitespace). For changed files, the system uses fuzzy matching with "
|
||||
"85% similarity threshold for first-time edits. 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;
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> EditFileTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> LLMQore::ToolResult {
|
||||
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 LLMQore::ToolInvalidArgument("'filename' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
if (newContent.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument("'new_content' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
|
||||
QFileInfo fileInfo(filename);
|
||||
QString filePath;
|
||||
|
||||
if (fileInfo.isAbsolute()) {
|
||||
filePath = filename;
|
||||
} else {
|
||||
QString projectRoot = Context::ProjectUtils::getProjectRoot();
|
||||
if (projectRoot.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Cannot resolve relative path '%1': no project is open. "
|
||||
"Please provide an absolute path or open a project.")
|
||||
.arg(filename));
|
||||
}
|
||||
|
||||
filePath = QDir(projectRoot).absoluteFilePath(filename);
|
||||
LOG_MESSAGE(QString("EditFileTool: Resolved relative path '%1' to '%2'")
|
||||
.arg(filename, filePath));
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.exists()) {
|
||||
throw LLMQore::ToolRuntimeError(QString("File does not exist: %1").arg(filePath));
|
||||
}
|
||||
|
||||
QFileInfo finalFileInfo(filePath);
|
||||
if (!finalFileInfo.isWritable()) {
|
||||
throw LLMQore::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()) {
|
||||
const QString projectRoot = Context::ProjectUtils::getProjectRoot();
|
||||
const QString hint = projectRoot.isEmpty()
|
||||
? QStringLiteral(
|
||||
"No project is currently open. Open a project in Qt Creator or "
|
||||
"enable 'Allow file access outside project' in QodeAssist settings.")
|
||||
: QString(
|
||||
"Retry with a path under the active project root: '%1'. The build "
|
||||
"directory is for compiler output only — source files must live under "
|
||||
"the project root. If you really need to edit outside the project, "
|
||||
"enable 'Allow file access outside project' in QodeAssist settings.")
|
||||
.arg(projectRoot);
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("File path '%1' is not within the current project. %2")
|
||||
.arg(filePath, hint));
|
||||
}
|
||||
LOG_MESSAGE(QString("Editing file outside project scope: %1").arg(filePath));
|
||||
}
|
||||
|
||||
QString editId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
bool autoApply = Settings::toolsSettings().autoApplyFileEdits();
|
||||
|
||||
LOG_MESSAGE(QString("EditFileTool: Edit details for %1:").arg(filePath));
|
||||
LOG_MESSAGE(QString(" oldContent length: %1 chars").arg(oldContent.length()));
|
||||
LOG_MESSAGE(QString(" newContent length: %1 chars").arg(newContent.length()));
|
||||
if (oldContent.length() <= 200) {
|
||||
LOG_MESSAGE(QString(" oldContent: '%1'").arg(oldContent));
|
||||
} else {
|
||||
LOG_MESSAGE(QString(" oldContent (first 200 chars): '%1...'")
|
||||
.arg(oldContent.left(200)));
|
||||
}
|
||||
if (newContent.length() <= 200) {
|
||||
LOG_MESSAGE(QString(" newContent: '%1'").arg(newContent));
|
||||
} else {
|
||||
LOG_MESSAGE(QString(" newContent (first 200 chars): '%1...'")
|
||||
.arg(newContent.left(200)));
|
||||
}
|
||||
|
||||
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 LLMQore::ToolResult::text(resultStr);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
26
sources/tools/EditFileTool.hpp
Normal file
26
sources/tools/EditFileTool.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class EditFileTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EditFileTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
510
sources/tools/ExecuteTerminalCommandTool.cpp
Normal file
510
sources/tools/ExecuteTerminalCommandTool.cpp
Normal file
@@ -0,0 +1,510 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ExecuteTerminalCommandTool.hpp"
|
||||
|
||||
#include <logger/Logger.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QPromise>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QTimer>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
ExecuteTerminalCommandTool::ExecuteTerminalCommandTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::id() const
|
||||
{
|
||||
return "execute_terminal_command";
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::displayName() const
|
||||
{
|
||||
return "Executing terminal command";
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::description() const
|
||||
{
|
||||
return getCommandDescription();
|
||||
}
|
||||
|
||||
QJsonObject ExecuteTerminalCommandTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
|
||||
QJsonObject properties;
|
||||
properties["command"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
QString("Name of the executable to run, WITHOUT any arguments or flags. "
|
||||
"Must be exactly one of the allowed commands: %1. "
|
||||
"Put every flag and argument in the separate `args` field. "
|
||||
"Correct: command=\"ls\", args=\"-R\". "
|
||||
"Incorrect: command=\"ls -R\" (the whole line in one field will be rejected).")
|
||||
.arg(allowedList)}};
|
||||
|
||||
properties["args"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
"Optional arguments and flags for the command, as a single string. Do NOT repeat the "
|
||||
"command name here. Arguments with spaces should be quoted. "
|
||||
"Example: args=\"--file \\\"path with spaces.txt\\\" --verbose\"."}};
|
||||
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{"command"};
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
using LLMQore::ToolResult;
|
||||
|
||||
QString command = input.value("command").toString().trimmed();
|
||||
QString args = input.value("args").toString().trimmed();
|
||||
|
||||
if (command.isEmpty()) {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Command is empty");
|
||||
return QtFuture::makeReadyFuture(ToolResult::error("Error: Command parameter is required."));
|
||||
}
|
||||
|
||||
// Tolerate models that pack the whole command line into `command`. As long as `args` is
|
||||
// empty we can safely split on the first whitespace — the allowlist check still validates
|
||||
// the actual executable name.
|
||||
if (args.isEmpty()) {
|
||||
const int firstSpace = command.indexOf(QRegularExpression("\\s"));
|
||||
if (firstSpace > 0) {
|
||||
args = command.mid(firstSpace + 1).trimmed();
|
||||
command = command.left(firstSpace);
|
||||
}
|
||||
}
|
||||
|
||||
if (command.length() > MAX_COMMAND_LENGTH) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command too long (%1 chars)")
|
||||
.arg(command.length()));
|
||||
return QtFuture::makeReadyFuture(
|
||||
ToolResult::error(QString("Error: Command exceeds maximum length of %1 characters.")
|
||||
.arg(MAX_COMMAND_LENGTH)));
|
||||
}
|
||||
|
||||
if (args.length() > MAX_ARGS_LENGTH) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Arguments too long (%1 chars)")
|
||||
.arg(args.length()));
|
||||
return QtFuture::makeReadyFuture(
|
||||
ToolResult::error(QString("Error: Arguments exceed maximum length of %1 characters.")
|
||||
.arg(MAX_ARGS_LENGTH)));
|
||||
}
|
||||
|
||||
if (!isCommandAllowed(command)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' is not allowed")
|
||||
.arg(command));
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
return QtFuture::makeReadyFuture(
|
||||
ToolResult::error(QString("Error: Command '%1' is not in the allowed list. Allowed commands: %2")
|
||||
.arg(command)
|
||||
.arg(allowedList)));
|
||||
}
|
||||
|
||||
if (!isCommandSafe(command)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' contains unsafe characters")
|
||||
.arg(command));
|
||||
#ifdef Q_OS_WIN
|
||||
const QString allowedChars = "alphanumeric characters, hyphens, underscores, dots, colons, "
|
||||
"backslashes, and forward slashes";
|
||||
#else
|
||||
const QString allowedChars = "alphanumeric characters, hyphens, underscores, dots, and slashes";
|
||||
#endif
|
||||
return QtFuture::makeReadyFuture(
|
||||
ToolResult::error(QString("Error: Command '%1' contains potentially dangerous characters. "
|
||||
"Only %2 are allowed.")
|
||||
.arg(command)
|
||||
.arg(allowedChars)));
|
||||
}
|
||||
|
||||
if (!args.isEmpty() && !areArgumentsSafe(args)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Arguments contain unsafe patterns: '%1'")
|
||||
.arg(args));
|
||||
return QtFuture::makeReadyFuture(
|
||||
ToolResult::error(QString("Error: Arguments contain potentially dangerous patterns (command chaining, "
|
||||
"redirection, or pipe operators).")));
|
||||
}
|
||||
|
||||
auto *project = ProjectExplorer::ProjectManager::startupProject();
|
||||
QString workingDir;
|
||||
|
||||
if (project) {
|
||||
workingDir = project->projectDirectory().toString();
|
||||
LOG_MESSAGE(
|
||||
QString("ExecuteTerminalCommandTool: Working directory is '%1'").arg(workingDir));
|
||||
} else {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: No active project, using current directory");
|
||||
workingDir = QDir::currentPath();
|
||||
}
|
||||
|
||||
QDir dir(workingDir);
|
||||
if (!dir.exists() || !dir.isReadable()) {
|
||||
LOG_MESSAGE(
|
||||
QString("ExecuteTerminalCommandTool: Working directory '%1' is not accessible")
|
||||
.arg(workingDir));
|
||||
return QtFuture::makeReadyFuture(
|
||||
ToolResult::error(QString("Error: Working directory '%1' does not exist or is not accessible.")
|
||||
.arg(workingDir)));
|
||||
}
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing command '%1' with args '%2' in '%3'")
|
||||
.arg(command)
|
||||
.arg(args.isEmpty() ? "(no args)" : args)
|
||||
.arg(workingDir));
|
||||
|
||||
auto promise = QSharedPointer<QPromise<ToolResult>>::create();
|
||||
QFuture<ToolResult> future = promise->future();
|
||||
promise->start();
|
||||
|
||||
auto resolved = std::make_shared<std::atomic<bool>>(false);
|
||||
|
||||
QProcess *process = new QProcess();
|
||||
process->setWorkingDirectory(workingDir);
|
||||
process->setProcessChannelMode(QProcess::MergedChannels);
|
||||
|
||||
process->setReadChannel(QProcess::StandardOutput);
|
||||
|
||||
const int timeoutMs = commandTimeoutMs();
|
||||
|
||||
QTimer *timeoutTimer = new QTimer();
|
||||
timeoutTimer->setSingleShot(true);
|
||||
timeoutTimer->setInterval(timeoutMs);
|
||||
|
||||
QObject::connect(timeoutTimer, &QTimer::timeout, [process, promise, resolved, command, args, timeoutTimer, timeoutMs]() {
|
||||
if (*resolved)
|
||||
return;
|
||||
*resolved = true;
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1 %2' timed out after %3ms")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(timeoutMs));
|
||||
|
||||
process->terminate();
|
||||
|
||||
QTimer::singleShot(1000, process, [process]() {
|
||||
if (process->state() != QProcess::NotRunning) {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Forcefully killing process after timeout");
|
||||
process->kill();
|
||||
}
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
promise->addResult(ToolResult::error(QString("Error: Command '%1 %2' timed out after %3 seconds. "
|
||||
"The process has been terminated.")
|
||||
.arg(command)
|
||||
.arg(args.isEmpty() ? "" : args)
|
||||
.arg(timeoutMs / 1000)));
|
||||
promise->finish();
|
||||
timeoutTimer->deleteLater();
|
||||
});
|
||||
|
||||
QObject::connect(
|
||||
process,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
[this, process, promise, resolved, command, args, timeoutTimer](
|
||||
int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
if (*resolved) {
|
||||
process->deleteLater();
|
||||
return;
|
||||
}
|
||||
*resolved = true;
|
||||
|
||||
timeoutTimer->stop();
|
||||
timeoutTimer->deleteLater();
|
||||
|
||||
const QByteArray rawOutput = process->readAll();
|
||||
const qint64 outputSize = rawOutput.size();
|
||||
const QString output = sanitizeOutput(QString::fromUtf8(rawOutput), outputSize);
|
||||
|
||||
const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args);
|
||||
|
||||
if (exitStatus == QProcess::NormalExit) {
|
||||
if (exitCode == 0) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' completed "
|
||||
"successfully (output size: %2 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(outputSize));
|
||||
promise->addResult(ToolResult::text(
|
||||
QString("Command '%1' executed successfully.\n\nOutput:\n%2")
|
||||
.arg(fullCommand)
|
||||
.arg(output.isEmpty() ? "(no output)" : output)));
|
||||
} else {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' failed with "
|
||||
"exit code %2 (output size: %3 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(exitCode)
|
||||
.arg(outputSize));
|
||||
promise->addResult(ToolResult::error(
|
||||
QString("Command '%1' failed with exit code %2.\n\nOutput:\n%3")
|
||||
.arg(fullCommand)
|
||||
.arg(exitCode)
|
||||
.arg(output.isEmpty() ? "(no output)" : output)));
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' crashed or was "
|
||||
"terminated (output size: %2 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(outputSize));
|
||||
const QString error = process->errorString();
|
||||
promise->addResult(ToolResult::error(
|
||||
QString("Command '%1' crashed or was terminated.\n\nError: %2\n\nOutput:\n%3")
|
||||
.arg(fullCommand)
|
||||
.arg(error)
|
||||
.arg(output.isEmpty() ? "(no output)" : output)));
|
||||
}
|
||||
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
QObject::connect(process, &QProcess::errorOccurred, [process, promise, resolved, command, args, timeoutTimer](
|
||||
QProcess::ProcessError error) {
|
||||
if (*resolved) {
|
||||
process->deleteLater();
|
||||
return;
|
||||
}
|
||||
*resolved = true;
|
||||
|
||||
timeoutTimer->stop();
|
||||
timeoutTimer->deleteLater();
|
||||
|
||||
const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args);
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process error occurred for '%1': %2 (%3)")
|
||||
.arg(fullCommand)
|
||||
.arg(error)
|
||||
.arg(process->errorString()));
|
||||
|
||||
QString errorMessage;
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
errorMessage = QString("Failed to start command '%1'. The command may not exist or "
|
||||
"you may not have permission to execute it.")
|
||||
.arg(fullCommand);
|
||||
break;
|
||||
case QProcess::Crashed:
|
||||
errorMessage = QString("Command '%1' crashed during execution.").arg(fullCommand);
|
||||
break;
|
||||
case QProcess::Timedout:
|
||||
errorMessage = QString("Command '%1' timed out.").arg(fullCommand);
|
||||
break;
|
||||
case QProcess::WriteError:
|
||||
errorMessage = QString("Write error occurred while executing '%1'.").arg(fullCommand);
|
||||
break;
|
||||
case QProcess::ReadError:
|
||||
errorMessage = QString("Read error occurred while executing '%1'.").arg(fullCommand);
|
||||
break;
|
||||
default:
|
||||
errorMessage = QString("Unknown error occurred while executing '%1': %2")
|
||||
.arg(fullCommand)
|
||||
.arg(process->errorString());
|
||||
break;
|
||||
}
|
||||
|
||||
promise->addResult(ToolResult::error(QString("Error: %1").arg(errorMessage)));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
QStringList argsList;
|
||||
if (!args.isEmpty()) {
|
||||
argsList = QProcess::splitCommand(args);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
static const QStringList windowsBuiltinCommands = {
|
||||
"dir", "type", "del", "copy", "move", "ren", "rename",
|
||||
"md", "mkdir", "rd", "rmdir", "cd", "chdir", "cls", "echo",
|
||||
"set", "path", "prompt", "ver", "vol", "date", "time"
|
||||
};
|
||||
|
||||
const QString lowerCommand = command.toLower();
|
||||
const bool isBuiltin = windowsBuiltinCommands.contains(lowerCommand);
|
||||
|
||||
if (isBuiltin) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing Windows builtin command '%1' via cmd.exe")
|
||||
.arg(command));
|
||||
QStringList cmdArgs;
|
||||
cmdArgs << "/c" << command;
|
||||
cmdArgs.append(argsList);
|
||||
process->start("cmd.exe", cmdArgs);
|
||||
} else {
|
||||
process->start(command, argsList);
|
||||
}
|
||||
#else
|
||||
process->start(command, argsList);
|
||||
#endif
|
||||
|
||||
timeoutTimer->start();
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process start requested for '%1'")
|
||||
.arg(command));
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
bool ExecuteTerminalCommandTool::isCommandAllowed(const QString &command) const
|
||||
{
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
return allowed.contains(command, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool ExecuteTerminalCommandTool::isCommandSafe(const QString &command) const
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
static const QRegularExpression safePattern("^[a-zA-Z0-9._/\\\\:-]+$");
|
||||
#else
|
||||
static const QRegularExpression safePattern("^[a-zA-Z0-9._/-]+$");
|
||||
#endif
|
||||
|
||||
const bool isSafe = safePattern.match(command).hasMatch();
|
||||
if (!isSafe) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' failed safety check")
|
||||
.arg(command));
|
||||
}
|
||||
return isSafe;
|
||||
}
|
||||
|
||||
bool ExecuteTerminalCommandTool::areArgumentsSafe(const QString &args) const
|
||||
{
|
||||
if (args.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for null bytes
|
||||
if (args.contains(QChar('\0'))) {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Null byte found in args");
|
||||
return false;
|
||||
}
|
||||
|
||||
static const QStringList dangerousPatterns = {
|
||||
";", // Command separator
|
||||
"&", // Command separator / background execution
|
||||
"|", // Pipe operator
|
||||
">", // Output redirection
|
||||
"<", // Input redirection
|
||||
"`", // Command substitution
|
||||
"$(", // Command substitution
|
||||
"${", // Variable expansion
|
||||
"\n", // Newline (could start new command)
|
||||
"\r", // Carriage return
|
||||
#ifdef Q_OS_WIN
|
||||
"^", // Escape character in cmd.exe (can bypass other checks)
|
||||
"%", // Environment variable expansion on Windows
|
||||
#endif
|
||||
};
|
||||
|
||||
for (const QString &pattern : dangerousPatterns) {
|
||||
if (args.contains(pattern)) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Dangerous pattern '%1' found in args")
|
||||
.arg(pattern));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::sanitizeOutput(const QString &output, qint64 totalSize) const
|
||||
{
|
||||
if (totalSize > MAX_OUTPUT_SIZE) {
|
||||
const QString truncated = output.left(MAX_OUTPUT_SIZE / 2);
|
||||
return QString("%1\n\n... [Output truncated: exceeded maximum size of %2 MB. "
|
||||
"Total output size was %3 bytes] ...")
|
||||
.arg(truncated)
|
||||
.arg(MAX_OUTPUT_SIZE / (1024 * 1024))
|
||||
.arg(totalSize);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
|
||||
{
|
||||
QString commandsStr;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed();
|
||||
#elif defined(Q_OS_MACOS)
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsMacOS().trimmed();
|
||||
#elif defined(Q_OS_WIN)
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsWindows().trimmed();
|
||||
#else
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed(); // fallback
|
||||
#endif
|
||||
|
||||
if (commandsStr.isEmpty()) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
QStringList result;
|
||||
const QStringList rawCommands = commandsStr.split(',', Qt::SkipEmptyParts);
|
||||
result.reserve(rawCommands.size());
|
||||
|
||||
for (const QString &cmd : rawCommands) {
|
||||
const QString trimmed = cmd.trimmed();
|
||||
if (!trimmed.isEmpty()) {
|
||||
result.append(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int ExecuteTerminalCommandTool::commandTimeoutMs() const
|
||||
{
|
||||
return Settings::toolsSettings().terminalCommandTimeout() * 1000;
|
||||
}
|
||||
|
||||
QString ExecuteTerminalCommandTool::getCommandDescription() const
|
||||
{
|
||||
const QStringList allowed = getAllowedCommands();
|
||||
const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", ");
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
const QString osInfo = " Running on Linux.";
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const QString osInfo = " Running on macOS.";
|
||||
#elif defined(Q_OS_WIN)
|
||||
const QString osInfo = " Running on Windows.";
|
||||
#else
|
||||
const QString osInfo = "";
|
||||
#endif
|
||||
|
||||
return QString(
|
||||
"Execute a terminal command in the project directory. "
|
||||
"Only commands from the allowed list can be executed. "
|
||||
"Currently allowed commands for this OS: %1. "
|
||||
"The command will be executed in the root directory of the active project. "
|
||||
"Commands have a %2 second timeout. "
|
||||
"Returns the command output (stdout and stderr) or an error message if the command fails.%3")
|
||||
.arg(allowedList)
|
||||
.arg(commandTimeoutMs() / 1000)
|
||||
.arg(osInfo);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
42
sources/tools/ExecuteTerminalCommandTool.hpp
Normal file
42
sources/tools/ExecuteTerminalCommandTool.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class ExecuteTerminalCommandTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ExecuteTerminalCommandTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
private:
|
||||
bool isCommandAllowed(const QString &command) const;
|
||||
bool isCommandSafe(const QString &command) const;
|
||||
bool areArgumentsSafe(const QString &args) const;
|
||||
QStringList getAllowedCommands() const;
|
||||
QString getCommandDescription() const;
|
||||
QString sanitizeOutput(const QString &output, qint64 maxSize) const;
|
||||
|
||||
int commandTimeoutMs() const;
|
||||
|
||||
// Constants for production safety
|
||||
static constexpr qint64 MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
static constexpr int MAX_COMMAND_LENGTH = 1024;
|
||||
static constexpr int MAX_ARGS_LENGTH = 4096;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
243
sources/tools/FileSearchUtils.cpp
Normal file
243
sources/tools/FileSearchUtils.cpp
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "FileSearchUtils.hpp"
|
||||
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <settings/GeneralSettings.hpp>
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QTextStream>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
FileSearchUtils::FileMatch FileSearchUtils::findBestMatch(
|
||||
const QString &query,
|
||||
const QString &filePattern,
|
||||
int maxResults,
|
||||
Context::IgnoreManager *ignoreManager)
|
||||
{
|
||||
QList<FileMatch> candidates;
|
||||
auto projects = ProjectExplorer::ProjectManager::projects();
|
||||
|
||||
if (projects.isEmpty()) {
|
||||
return FileMatch{};
|
||||
}
|
||||
|
||||
QFileInfo queryInfo(query);
|
||||
if (queryInfo.isAbsolute() && queryInfo.exists() && queryInfo.isFile()) {
|
||||
FileMatch match;
|
||||
match.absolutePath = queryInfo.canonicalFilePath();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
if (match.absolutePath.startsWith(projectDir)) {
|
||||
match.relativePath = QDir(projectDir).relativeFilePath(match.absolutePath);
|
||||
match.projectName = project->displayName();
|
||||
match.matchType = MatchType::ExactName;
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
match.relativePath = queryInfo.fileName();
|
||||
match.projectName = "External";
|
||||
match.matchType = MatchType::ExactName;
|
||||
return match;
|
||||
}
|
||||
|
||||
QString lowerQuery = query.toLower();
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
QString projectName = project->displayName();
|
||||
|
||||
for (const auto &filePath : projectFiles) {
|
||||
QString absolutePath = filePath.path();
|
||||
|
||||
if (ignoreManager && ignoreManager->shouldIgnore(absolutePath, project))
|
||||
continue;
|
||||
|
||||
QFileInfo fileInfo(absolutePath);
|
||||
QString fileName = fileInfo.fileName();
|
||||
|
||||
if (!filePattern.isEmpty() && !matchesFilePattern(fileName, filePattern))
|
||||
continue;
|
||||
|
||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||
|
||||
FileMatch match;
|
||||
match.absolutePath = absolutePath;
|
||||
match.relativePath = relativePath;
|
||||
match.projectName = projectName;
|
||||
|
||||
QString lowerFileName = fileName.toLower();
|
||||
QString lowerRelativePath = relativePath.toLower();
|
||||
|
||||
if (lowerFileName == lowerQuery) {
|
||||
match.matchType = MatchType::ExactName;
|
||||
candidates.append(match);
|
||||
} else if (lowerRelativePath.contains(lowerQuery)) {
|
||||
match.matchType = MatchType::PathMatch;
|
||||
candidates.append(match);
|
||||
} else if (lowerFileName.contains(lowerQuery)) {
|
||||
match.matchType = MatchType::PartialName;
|
||||
candidates.append(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty() || candidates.first().matchType != MatchType::ExactName) {
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
QString projectName = project->displayName();
|
||||
int depth = 0;
|
||||
searchInFileSystem(
|
||||
projectDir,
|
||||
lowerQuery,
|
||||
projectName,
|
||||
projectDir,
|
||||
project,
|
||||
candidates,
|
||||
maxResults,
|
||||
depth,
|
||||
5,
|
||||
ignoreManager);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) {
|
||||
return FileMatch{};
|
||||
}
|
||||
|
||||
std::sort(candidates.begin(), candidates.end());
|
||||
return candidates.first();
|
||||
}
|
||||
|
||||
void FileSearchUtils::searchInFileSystem(
|
||||
const QString &dirPath,
|
||||
const QString &query,
|
||||
const QString &projectName,
|
||||
const QString &projectDir,
|
||||
ProjectExplorer::Project *project,
|
||||
QList<FileMatch> &matches,
|
||||
int maxResults,
|
||||
int ¤tDepth,
|
||||
int maxDepth,
|
||||
Context::IgnoreManager *ignoreManager)
|
||||
{
|
||||
if (currentDepth >= maxDepth || matches.size() >= maxResults)
|
||||
return;
|
||||
|
||||
currentDepth++;
|
||||
QDir dir(dirPath);
|
||||
if (!dir.exists()) {
|
||||
currentDepth--;
|
||||
return;
|
||||
}
|
||||
|
||||
auto entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const auto &entry : entries) {
|
||||
if (matches.size() >= maxResults)
|
||||
break;
|
||||
|
||||
QString absolutePath = entry.absoluteFilePath();
|
||||
|
||||
if (ignoreManager && ignoreManager->shouldIgnore(absolutePath, project))
|
||||
continue;
|
||||
|
||||
QString fileName = entry.fileName();
|
||||
|
||||
if (entry.isDir()) {
|
||||
searchInFileSystem(
|
||||
absolutePath,
|
||||
query,
|
||||
projectName,
|
||||
projectDir,
|
||||
project,
|
||||
matches,
|
||||
maxResults,
|
||||
currentDepth,
|
||||
maxDepth,
|
||||
ignoreManager);
|
||||
continue;
|
||||
}
|
||||
|
||||
QString lowerFileName = fileName.toLower();
|
||||
QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||
QString lowerRelativePath = relativePath.toLower();
|
||||
|
||||
FileMatch match;
|
||||
match.absolutePath = absolutePath;
|
||||
match.relativePath = relativePath;
|
||||
match.projectName = projectName;
|
||||
|
||||
if (lowerFileName == query) {
|
||||
match.matchType = MatchType::ExactName;
|
||||
matches.append(match);
|
||||
} else if (lowerRelativePath.contains(query)) {
|
||||
match.matchType = MatchType::PathMatch;
|
||||
matches.append(match);
|
||||
} else if (lowerFileName.contains(query)) {
|
||||
match.matchType = MatchType::PartialName;
|
||||
matches.append(match);
|
||||
}
|
||||
}
|
||||
|
||||
currentDepth--;
|
||||
}
|
||||
|
||||
bool FileSearchUtils::matchesFilePattern(const QString &fileName, const QString &pattern)
|
||||
{
|
||||
if (pattern.isEmpty())
|
||||
return true;
|
||||
|
||||
if (pattern.startsWith("*.")) {
|
||||
QString extension = pattern.mid(1);
|
||||
return fileName.endsWith(extension, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return fileName.compare(pattern, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
QString FileSearchUtils::readFileContent(const QString &filePath)
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString canonicalPath = QFileInfo(filePath).canonicalFilePath();
|
||||
bool isInProject = Context::ProjectUtils::isFileInProject(canonicalPath);
|
||||
|
||||
if (!isInProject) {
|
||||
const auto &settings = Settings::toolsSettings();
|
||||
if (!settings.allowAccessOutsideProject()) {
|
||||
LOG_MESSAGE(QString("Access denied to file outside project: %1").arg(canonicalPath));
|
||||
return QString();
|
||||
}
|
||||
LOG_MESSAGE(QString("Reading file outside project scope: %1").arg(canonicalPath));
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream.setAutoDetectUnicode(true);
|
||||
QString content = stream.readAll();
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
137
sources/tools/FileSearchUtils.hpp
Normal file
137
sources/tools/FileSearchUtils.hpp
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
namespace ProjectExplorer {
|
||||
class Project;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
/**
|
||||
* @brief Utility class for file searching and reading operations
|
||||
*
|
||||
* Provides common functionality for file operations used by various tools:
|
||||
* - Fuzzy file searching with multiple match strategies
|
||||
* - File pattern matching (e.g., *.cpp, *.h)
|
||||
* - Secure file content reading with project boundary checks
|
||||
* - Integration with IgnoreManager for respecting .qodeassistignore
|
||||
*/
|
||||
class FileSearchUtils
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Match quality levels for file search results
|
||||
*/
|
||||
enum class MatchType {
|
||||
ExactName, ///< Exact filename match (highest priority)
|
||||
PathMatch, ///< Query found in relative path
|
||||
PartialName ///< Query found in filename (lowest priority)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a file search result with metadata
|
||||
*/
|
||||
struct FileMatch
|
||||
{
|
||||
QString absolutePath; ///< Full absolute path to the file
|
||||
QString relativePath; ///< Path relative to project root
|
||||
QString projectName; ///< Name of the project containing the file
|
||||
QString content; ///< File content (if read)
|
||||
MatchType matchType; ///< Quality of the match
|
||||
bool contentRead = false; ///< Whether content has been read
|
||||
QString error; ///< Error message if operation failed
|
||||
|
||||
/**
|
||||
* @brief Compare matches by quality (for sorting)
|
||||
*/
|
||||
bool operator<(const FileMatch &other) const
|
||||
{
|
||||
return static_cast<int>(matchType) < static_cast<int>(other.matchType);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Find the best matching file across all open projects
|
||||
*
|
||||
* Search strategy:
|
||||
* 1. Check if query is an absolute path
|
||||
* 2. Search in project source files (exact, path, partial matches)
|
||||
* 3. Search filesystem within project directories (respects .qodeassistignore)
|
||||
*
|
||||
* @param query Filename, partial name, or path to search for (case-insensitive)
|
||||
* @param filePattern Optional file pattern filter (e.g., "*.cpp", "*.h")
|
||||
* @param maxResults Maximum number of candidates to collect
|
||||
* @param ignoreManager IgnoreManager instance for filtering files
|
||||
* @return Best matching file, or empty FileMatch if not found
|
||||
*/
|
||||
static FileMatch findBestMatch(
|
||||
const QString &query,
|
||||
const QString &filePattern = QString(),
|
||||
int maxResults = 10,
|
||||
Context::IgnoreManager *ignoreManager = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Check if a filename matches a file pattern
|
||||
*
|
||||
* Supports:
|
||||
* - Wildcard patterns (*.cpp, *.h)
|
||||
* - Exact filename matching
|
||||
* - Empty pattern (matches all)
|
||||
*
|
||||
* @param fileName File name to check
|
||||
* @param pattern Pattern to match against
|
||||
* @return true if filename matches pattern
|
||||
*/
|
||||
static bool matchesFilePattern(const QString &fileName, const QString &pattern);
|
||||
|
||||
/**
|
||||
* @brief Read file content with security checks
|
||||
*
|
||||
* Performs the following checks:
|
||||
* - File exists and is readable
|
||||
* - Respects project boundary settings (allowAccessOutsideProject)
|
||||
* - Logs access to files outside project scope
|
||||
*
|
||||
* @param filePath Absolute path to file
|
||||
* @return File content as QString, or null QString on error
|
||||
*/
|
||||
static QString readFileContent(const QString &filePath);
|
||||
|
||||
/**
|
||||
* @brief Search for files in filesystem directory tree
|
||||
*
|
||||
* Recursively searches a directory for files matching the query.
|
||||
* Respects .qodeassistignore patterns and depth limits.
|
||||
*
|
||||
* @param dirPath Directory to search in
|
||||
* @param query Search query (case-insensitive)
|
||||
* @param projectName Name of the project for metadata
|
||||
* @param projectDir Root directory of the project
|
||||
* @param project Project instance for ignore checking
|
||||
* @param matches Output list to append matches to
|
||||
* @param maxResults Stop after finding this many matches
|
||||
* @param currentDepth Current recursion depth (modified during recursion)
|
||||
* @param maxDepth Maximum recursion depth
|
||||
* @param ignoreManager IgnoreManager instance for filtering files
|
||||
*/
|
||||
static void searchInFileSystem(
|
||||
const QString &dirPath,
|
||||
const QString &query,
|
||||
const QString &projectName,
|
||||
const QString &projectDir,
|
||||
ProjectExplorer::Project *project,
|
||||
QList<FileMatch> &matches,
|
||||
int maxResults,
|
||||
int ¤tDepth,
|
||||
int maxDepth = 5,
|
||||
Context::IgnoreManager *ignoreManager = nullptr);
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
93
sources/tools/FindFileTool.cpp
Normal file
93
sources/tools/FindFileTool.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "FindFileTool.hpp"
|
||||
|
||||
#include "FileSearchUtils.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <logger/Logger.hpp>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
FindFileTool::FindFileTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
, m_ignoreManager(new Context::IgnoreManager(this))
|
||||
{}
|
||||
|
||||
QString FindFileTool::id() const
|
||||
{
|
||||
return "find_file";
|
||||
}
|
||||
|
||||
QString FindFileTool::displayName() const
|
||||
{
|
||||
return "Finding file";
|
||||
}
|
||||
|
||||
QString FindFileTool::description() const
|
||||
{
|
||||
return "Find a file in the current project(s) by name or partial path. "
|
||||
"Returns the absolute path, project-relative path, and the name of the project "
|
||||
"that contains the match. Does NOT read file content — use `read_file` separately "
|
||||
"when you need the content. Use this when you know part of the filename but not "
|
||||
"the exact location.";
|
||||
}
|
||||
|
||||
QJsonObject FindFileTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
properties["query"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
"Filename, partial filename, or partial path to look for. Case-insensitive. "
|
||||
"Exact filename matches rank highest, then path matches, then partial name matches."}};
|
||||
|
||||
properties["file_pattern"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
"Optional wildcard filter applied to the filename. Examples: '*.cpp', '*.h', '*.qml'. "
|
||||
"Leave empty to match any extension."}};
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{"query"};
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> FindFileTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> LLMQore::ToolResult {
|
||||
QString query = input["query"].toString().trimmed();
|
||||
if (query.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument("'query' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
QString filePattern = input["file_pattern"].toString();
|
||||
|
||||
LOG_MESSAGE(QString("FindFileTool: Searching for '%1' (pattern: %2)")
|
||||
.arg(query, filePattern.isEmpty() ? "none" : filePattern));
|
||||
|
||||
FileSearchUtils::FileMatch match
|
||||
= FileSearchUtils::findBestMatch(query, filePattern, 10, m_ignoreManager);
|
||||
|
||||
if (match.absolutePath.isEmpty()) {
|
||||
return LLMQore::ToolResult::text(QString("No file found matching '%1'").arg(query));
|
||||
}
|
||||
|
||||
QString result = QString("Found file: %1\nAbsolute path: %2\nProject: %3")
|
||||
.arg(match.relativePath, match.absolutePath, match.projectName);
|
||||
|
||||
return LLMQore::ToolResult::text(result);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
32
sources/tools/FindFileTool.hpp
Normal file
32
sources/tools/FindFileTool.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
#include <QFuture>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class FindFileTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FindFileTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input) override;
|
||||
|
||||
private:
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
228
sources/tools/GetIssuesListTool.cpp
Normal file
228
sources/tools/GetIssuesListTool.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "GetIssuesListTool.hpp"
|
||||
|
||||
#include "plugin/Version.hpp"
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/taskhub.h>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMutexLocker>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
IssuesTracker &IssuesTracker::instance()
|
||||
{
|
||||
static IssuesTracker tracker;
|
||||
return tracker;
|
||||
}
|
||||
|
||||
IssuesTracker::IssuesTracker(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
|
||||
auto &hub = ProjectExplorer::taskHub();
|
||||
|
||||
connect(&hub, &ProjectExplorer::TaskHub::taskAdded, this, &IssuesTracker::onTaskAdded);
|
||||
connect(&hub, &ProjectExplorer::TaskHub::taskRemoved, this, &IssuesTracker::onTaskRemoved);
|
||||
connect(&hub, &ProjectExplorer::TaskHub::tasksCleared, this, &IssuesTracker::onTasksCleared);
|
||||
|
||||
}
|
||||
|
||||
QList<ProjectExplorer::Task> IssuesTracker::getTasks() const
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
return m_tasks;
|
||||
}
|
||||
|
||||
void IssuesTracker::onTaskAdded(const ProjectExplorer::Task &task)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_tasks.append(task);
|
||||
|
||||
QString typeStr;
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0)
|
||||
auto taskType = task.type();
|
||||
auto taskFile = task.file();
|
||||
auto taskLine = task.line();
|
||||
#else
|
||||
auto taskType = task.type;
|
||||
auto taskFile = task.file;
|
||||
auto taskLine = task.line;
|
||||
#endif
|
||||
switch (taskType) {
|
||||
case ProjectExplorer::Task::Error:
|
||||
typeStr = "ERROR";
|
||||
break;
|
||||
case ProjectExplorer::Task::Warning:
|
||||
typeStr = "WARNING";
|
||||
break;
|
||||
default:
|
||||
typeStr = "INFO";
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void IssuesTracker::onTaskRemoved(const ProjectExplorer::Task &task)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_tasks.removeOne(task);
|
||||
|
||||
}
|
||||
|
||||
void IssuesTracker::onTasksCleared(Utils::Id categoryId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
if (categoryId.isValid()) {
|
||||
int beforeCount = m_tasks.size();
|
||||
m_tasks.erase(
|
||||
std::remove_if(
|
||||
m_tasks.begin(),
|
||||
m_tasks.end(),
|
||||
[categoryId](const ProjectExplorer::Task &task) {
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0)
|
||||
return task.category() == categoryId;
|
||||
#else
|
||||
return task.category == categoryId;
|
||||
#endif
|
||||
}),
|
||||
m_tasks.end());
|
||||
int removedCount = beforeCount - m_tasks.size();
|
||||
|
||||
} else {
|
||||
int clearedCount = m_tasks.size();
|
||||
m_tasks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
GetIssuesListTool::GetIssuesListTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{
|
||||
IssuesTracker::instance();
|
||||
}
|
||||
|
||||
QString GetIssuesListTool::id() const
|
||||
{
|
||||
return "get_issues_list";
|
||||
}
|
||||
|
||||
QString GetIssuesListTool::displayName() const
|
||||
{
|
||||
return "Getting issues list from Qt Creator";
|
||||
}
|
||||
|
||||
QString GetIssuesListTool::description() const
|
||||
{
|
||||
return "Read diagnostics from Qt Creator's Issues panel, including the latest build output and "
|
||||
"live clang-codemodel warnings/errors for open files. Each issue includes file path, "
|
||||
"line number, severity, and message. Run `build_project` first if you need fresh build "
|
||||
"diagnostics.";
|
||||
}
|
||||
|
||||
QJsonObject GetIssuesListTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
|
||||
QJsonObject properties;
|
||||
properties["severity"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description", "Filter by severity: 'error', 'warning', or 'all'"},
|
||||
{"enum", QJsonArray{"error", "warning", "all"}}};
|
||||
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray();
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> GetIssuesListTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([input]() -> LLMQore::ToolResult {
|
||||
|
||||
QString severityFilter = input.value("severity").toString("all");
|
||||
|
||||
const auto tasks = IssuesTracker::instance().getTasks();
|
||||
|
||||
if (tasks.isEmpty()) {
|
||||
return LLMQore::ToolResult::text("No issues found in Qt Creator Issues panel.");
|
||||
}
|
||||
|
||||
QStringList results;
|
||||
results.append(QString("Total issues in panel: %1\n").arg(tasks.size()));
|
||||
|
||||
int errorCount = 0;
|
||||
int warningCount = 0;
|
||||
int processedCount = 0;
|
||||
|
||||
for (const ProjectExplorer::Task &task : tasks) {
|
||||
|
||||
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 0)
|
||||
auto taskType = task.type();
|
||||
auto taskFile = task.file();
|
||||
auto taskLine = task.line();
|
||||
auto taskColumn = task.column();
|
||||
auto taskCategory = task.category();
|
||||
#else
|
||||
auto taskType = task.type;
|
||||
auto taskFile = task.file;
|
||||
auto taskLine = task.line;
|
||||
auto taskColumn = task.column;
|
||||
auto taskCategory = task.category;
|
||||
#endif
|
||||
if (severityFilter == "error" && taskType != ProjectExplorer::Task::Error)
|
||||
continue;
|
||||
if (severityFilter == "warning" && taskType != ProjectExplorer::Task::Warning)
|
||||
continue;
|
||||
|
||||
QString typeStr;
|
||||
switch (taskType) {
|
||||
case ProjectExplorer::Task::Error:
|
||||
typeStr = "ERROR";
|
||||
errorCount++;
|
||||
break;
|
||||
case ProjectExplorer::Task::Warning:
|
||||
typeStr = "WARNING";
|
||||
warningCount++;
|
||||
break;
|
||||
default:
|
||||
typeStr = "INFO";
|
||||
break;
|
||||
}
|
||||
|
||||
QString issueText = QString("[%1] %2").arg(typeStr, task.description());
|
||||
|
||||
if (!taskFile.isEmpty()) {
|
||||
issueText += QString("\n File: %1").arg(taskFile.toUrlishString());
|
||||
if (taskLine > 0) {
|
||||
issueText += QString(":%1").arg(taskLine);
|
||||
if (taskColumn > 0) {
|
||||
issueText += QString(":%1").arg(taskColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!taskCategory.toString().isEmpty()) {
|
||||
issueText += QString("\n Category: %1").arg(taskCategory.toString());
|
||||
}
|
||||
|
||||
results.append(issueText);
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
QString summary = QString("\nSummary: %1 errors, %2 warnings (processed %3 tasks)")
|
||||
.arg(errorCount)
|
||||
.arg(warningCount)
|
||||
.arg(processedCount);
|
||||
results.prepend(summary);
|
||||
|
||||
return LLMQore::ToolResult::text(results.join("\n\n"));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
48
sources/tools/GetIssuesListTool.hpp
Normal file
48
sources/tools/GetIssuesListTool.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
#include <projectexplorer/task.h>
|
||||
#include <QList>
|
||||
#include <QMutex>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class IssuesTracker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static IssuesTracker &instance();
|
||||
|
||||
QList<ProjectExplorer::Task> getTasks() const;
|
||||
|
||||
private:
|
||||
explicit IssuesTracker(QObject *parent = nullptr);
|
||||
~IssuesTracker() override = default;
|
||||
|
||||
void onTaskAdded(const ProjectExplorer::Task &task);
|
||||
void onTaskRemoved(const ProjectExplorer::Task &task);
|
||||
void onTasksCleared(Utils::Id categoryId);
|
||||
|
||||
QList<ProjectExplorer::Task> m_tasks;
|
||||
mutable QMutex m_mutex;
|
||||
};
|
||||
|
||||
class GetIssuesListTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GetIssuesListTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
129
sources/tools/ListProjectFilesTool.cpp
Normal file
129
sources/tools/ListProjectFilesTool.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
ListProjectFilesTool::ListProjectFilesTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
, m_ignoreManager(new Context::IgnoreManager(this))
|
||||
|
||||
{}
|
||||
|
||||
QString ListProjectFilesTool::id() const
|
||||
{
|
||||
return "list_project_files";
|
||||
}
|
||||
|
||||
QString ListProjectFilesTool::displayName() const
|
||||
{
|
||||
return {"Reading project files list"};
|
||||
}
|
||||
|
||||
QString ListProjectFilesTool::description() const
|
||||
{
|
||||
return "List every source file tracked by the active Qt Creator project(s), filtered by "
|
||||
".qodeassistignore. Returns absolute and project-relative paths grouped by project. "
|
||||
"Useful for discovering the project layout before running focused searches or reads. "
|
||||
"Takes no parameters.";
|
||||
}
|
||||
|
||||
QJsonObject ListProjectFilesTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = QJsonObject();
|
||||
definition["required"] = QJsonArray();
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> ListProjectFilesTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
Q_UNUSED(input)
|
||||
|
||||
return QtConcurrent::run([this]() -> LLMQore::ToolResult {
|
||||
QList<ProjectExplorer::Project *> projects = ProjectExplorer::ProjectManager::projects();
|
||||
if (projects.isEmpty()) {
|
||||
QString error = "No projects found";
|
||||
throw LLMQore::ToolRuntimeError(error);
|
||||
}
|
||||
|
||||
QString result;
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
Utils::FilePaths projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
|
||||
if (projectFiles.isEmpty()) {
|
||||
result += QString("Project '%1': No source files found\n\n")
|
||||
.arg(project->displayName());
|
||||
continue;
|
||||
}
|
||||
|
||||
QStringList fileList;
|
||||
QString projectPath = project->projectDirectory().toUrlishString();
|
||||
QString projectAbsolutePath = project->projectDirectory().toFSPathString();
|
||||
|
||||
for (const auto &filePath : projectFiles) {
|
||||
QString absolutePath = filePath.toUrlishString();
|
||||
|
||||
if (m_ignoreManager->shouldIgnore(absolutePath, project)) {
|
||||
LOG_MESSAGE(
|
||||
QString("Ignoring file due to .qodeassistignore: %1").arg(absolutePath));
|
||||
continue;
|
||||
}
|
||||
|
||||
QString relativePath = QDir(projectPath).relativeFilePath(absolutePath);
|
||||
fileList.append(relativePath);
|
||||
}
|
||||
|
||||
if (fileList.isEmpty()) {
|
||||
result += QString("Project '%1': No files after applying .qodeassistignore\n\n")
|
||||
.arg(project->displayName());
|
||||
continue;
|
||||
}
|
||||
|
||||
fileList.sort();
|
||||
|
||||
result += QString("Project '%1' (%2 files):\n")
|
||||
.arg(project->displayName())
|
||||
.arg(fileList.size());
|
||||
result += QString("Project root: %1\n\n").arg(projectAbsolutePath);
|
||||
for (const QString &file : fileList) {
|
||||
result += QString("- %1\n").arg(file);
|
||||
}
|
||||
result += "\n";
|
||||
}
|
||||
|
||||
return LLMQore::ToolResult::text(result.trimmed());
|
||||
});
|
||||
}
|
||||
|
||||
QString ListProjectFilesTool::formatFileList(const QStringList &files) const
|
||||
{
|
||||
QString result = QString("Project files (%1 total):\n\n").arg(files.size());
|
||||
|
||||
for (const QString &file : files) {
|
||||
result += QString("- %1\n").arg(file);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
31
sources/tools/ListProjectFilesTool.hpp
Normal file
31
sources/tools/ListProjectFilesTool.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class ListProjectFilesTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ListProjectFilesTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
private:
|
||||
QString formatFileList(const QStringList &files) const;
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
340
sources/tools/ProjectSearchTool.cpp
Normal file
340
sources/tools/ProjectSearchTool.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ProjectSearchTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <cplusplus/Overview.h>
|
||||
#include <cplusplus/Scope.h>
|
||||
#include <cplusplus/Symbols.h>
|
||||
#include <cppeditor/cppmodelmanager.h>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
ProjectSearchTool::ProjectSearchTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
, m_ignoreManager(new Context::IgnoreManager(this))
|
||||
{}
|
||||
|
||||
QString ProjectSearchTool::id() const
|
||||
{
|
||||
return "search_project";
|
||||
}
|
||||
|
||||
QString ProjectSearchTool::displayName() const
|
||||
{
|
||||
return "Searching in project";
|
||||
}
|
||||
|
||||
QString ProjectSearchTool::description() const
|
||||
{
|
||||
return "Search across all open Qt Creator project(s) for text occurrences or C++ symbol "
|
||||
"definitions. 'text' mode scans source files line-by-line for literal text or regex. "
|
||||
"'symbol' mode uses Qt Creator's C++ code model and works only for C++ "
|
||||
"(not QML, Python, or plain text). Respects .qodeassistignore. "
|
||||
"Use `find_file` for locating files by name, not this tool.";
|
||||
}
|
||||
|
||||
QJsonObject ProjectSearchTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
properties["query"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description", "Text string or symbol name to search for. In text mode with "
|
||||
"use_regex=true, this is a regular expression."}};
|
||||
|
||||
properties["search_type"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"enum", QJsonArray{"text", "symbol"}},
|
||||
{"description", "Search mode: 'text' scans file contents, 'symbol' looks up C++ "
|
||||
"declarations via the code model."}};
|
||||
|
||||
properties["symbol_type"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"enum", QJsonArray{"all", "class", "function", "enum", "variable", "namespace"}},
|
||||
{"description", "Filter for symbol mode. Default: 'all'. Ignored in text mode."}};
|
||||
|
||||
properties["case_sensitive"] = QJsonObject{
|
||||
{"type", "boolean"},
|
||||
{"description", "Case-sensitive matching. Default: false."}};
|
||||
|
||||
properties["use_regex"] = QJsonObject{
|
||||
{"type", "boolean"},
|
||||
{"description", "Treat the query as a regular expression. Default: false."}};
|
||||
|
||||
properties["whole_words"] = QJsonObject{
|
||||
{"type", "boolean"},
|
||||
{"description", "Match whole words only. Text mode only; ignored in symbol mode. "
|
||||
"Default: false."}};
|
||||
|
||||
properties["file_pattern"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description", "Wildcard to restrict which files are searched in text mode, e.g. "
|
||||
"'*.cpp' or '*.h'. Default: all files."}};
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{"query", "search_type"};
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> ProjectSearchTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> LLMQore::ToolResult {
|
||||
QString query = input["query"].toString().trimmed();
|
||||
if (query.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument("Query parameter is required");
|
||||
}
|
||||
|
||||
QString searchTypeStr = input["search_type"].toString();
|
||||
if (searchTypeStr != "text" && searchTypeStr != "symbol") {
|
||||
throw LLMQore::ToolInvalidArgument("search_type must be 'text' or 'symbol'");
|
||||
}
|
||||
|
||||
SearchType searchType = (searchTypeStr == "symbol") ? SearchType::Symbol : SearchType::Text;
|
||||
QList<SearchResult> results;
|
||||
|
||||
if (searchType == SearchType::Text) {
|
||||
bool caseSensitive = input["case_sensitive"].toBool(false);
|
||||
bool useRegex = input["use_regex"].toBool(false);
|
||||
bool wholeWords = input["whole_words"].toBool(false);
|
||||
QString filePattern = input["file_pattern"].toString();
|
||||
|
||||
results = searchText(query, caseSensitive, useRegex, wholeWords, filePattern);
|
||||
} else {
|
||||
SymbolType symbolType = parseSymbolType(input["symbol_type"].toString());
|
||||
bool caseSensitive = input["case_sensitive"].toBool(false);
|
||||
bool useRegex = input["use_regex"].toBool(false);
|
||||
|
||||
results = searchSymbols(query, symbolType, caseSensitive, useRegex);
|
||||
}
|
||||
|
||||
if (results.isEmpty()) {
|
||||
return LLMQore::ToolResult::text(QString("No matches found for '%1'").arg(query));
|
||||
}
|
||||
|
||||
return LLMQore::ToolResult::text(formatResults(results, query));
|
||||
});
|
||||
}
|
||||
|
||||
QList<ProjectSearchTool::SearchResult> ProjectSearchTool::searchText(
|
||||
const QString &query,
|
||||
bool caseSensitive,
|
||||
bool useRegex,
|
||||
bool wholeWords,
|
||||
const QString &filePattern)
|
||||
{
|
||||
QList<SearchResult> results;
|
||||
auto projects = ProjectExplorer::ProjectManager::projects();
|
||||
if (projects.isEmpty())
|
||||
return results;
|
||||
|
||||
QRegularExpression searchRegex;
|
||||
if (useRegex) {
|
||||
QRegularExpression::PatternOptions options = QRegularExpression::MultilineOption;
|
||||
if (!caseSensitive)
|
||||
options |= QRegularExpression::CaseInsensitiveOption;
|
||||
searchRegex.setPattern(query);
|
||||
searchRegex.setPatternOptions(options);
|
||||
if (!searchRegex.isValid())
|
||||
return results;
|
||||
}
|
||||
|
||||
QRegularExpression fileFilter;
|
||||
if (!filePattern.isEmpty()) {
|
||||
fileFilter.setPattern(QRegularExpression::wildcardToRegularExpression(filePattern));
|
||||
}
|
||||
|
||||
for (auto project : projects) {
|
||||
if (!project)
|
||||
continue;
|
||||
|
||||
auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||
QString projectDir = project->projectDirectory().path();
|
||||
|
||||
for (const auto &filePath : projectFiles) {
|
||||
QString absolutePath = filePath.path();
|
||||
|
||||
if (m_ignoreManager->shouldIgnore(absolutePath, project))
|
||||
continue;
|
||||
|
||||
if (!filePattern.isEmpty()) {
|
||||
QFileInfo fileInfo(absolutePath);
|
||||
if (!fileFilter.match(fileInfo.fileName()).hasMatch())
|
||||
continue;
|
||||
}
|
||||
|
||||
QFile file(absolutePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
continue;
|
||||
|
||||
QTextStream stream(&file);
|
||||
int lineNumber = 0;
|
||||
while (!stream.atEnd()) {
|
||||
lineNumber++;
|
||||
QString line = stream.readLine();
|
||||
bool matched = false;
|
||||
|
||||
if (useRegex) {
|
||||
matched = searchRegex.match(line).hasMatch();
|
||||
} else {
|
||||
auto cs = caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
|
||||
if (wholeWords) {
|
||||
QRegularExpression wordRegex(
|
||||
QString("\\b%1\\b").arg(QRegularExpression::escape(query)),
|
||||
caseSensitive ? QRegularExpression::NoPatternOption
|
||||
: QRegularExpression::CaseInsensitiveOption);
|
||||
matched = wordRegex.match(line).hasMatch();
|
||||
} else {
|
||||
matched = line.contains(query, cs);
|
||||
}
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
SearchResult result;
|
||||
result.filePath = absolutePath;
|
||||
result.relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||
result.content = line.trimmed();
|
||||
result.lineNumber = lineNumber;
|
||||
results.append(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
QList<ProjectSearchTool::SearchResult> ProjectSearchTool::searchSymbols(
|
||||
const QString &query, SymbolType symbolType, bool caseSensitive, bool useRegex)
|
||||
{
|
||||
QList<SearchResult> results;
|
||||
auto modelManager = CppEditor::CppModelManager::instance();
|
||||
if (!modelManager)
|
||||
return results;
|
||||
|
||||
QRegularExpression searchRegex;
|
||||
if (useRegex) {
|
||||
QRegularExpression::PatternOptions options = caseSensitive
|
||||
? QRegularExpression::NoPatternOption
|
||||
: QRegularExpression::CaseInsensitiveOption;
|
||||
searchRegex.setPattern(query);
|
||||
searchRegex.setPatternOptions(options);
|
||||
if (!searchRegex.isValid())
|
||||
return results;
|
||||
}
|
||||
|
||||
CPlusPlus::Overview overview;
|
||||
auto snapshot = modelManager->snapshot();
|
||||
|
||||
for (auto it = snapshot.begin(); it != snapshot.end(); ++it) {
|
||||
auto document = it.value();
|
||||
if (!document || !document->globalNamespace())
|
||||
continue;
|
||||
|
||||
QString filePath = document->filePath().path();
|
||||
if (m_ignoreManager->shouldIgnore(filePath, nullptr))
|
||||
continue;
|
||||
|
||||
auto searchInScope = [&](auto self, CPlusPlus::Scope *scope) -> void {
|
||||
if (!scope)
|
||||
return;
|
||||
|
||||
for (unsigned i = 0; i < scope->memberCount(); ++i) {
|
||||
auto symbol = scope->memberAt(i);
|
||||
if (!symbol || !symbol->name())
|
||||
continue;
|
||||
|
||||
QString symbolName = overview.prettyName(symbol->name());
|
||||
bool nameMatches = false;
|
||||
|
||||
if (useRegex) {
|
||||
nameMatches = searchRegex.match(symbolName).hasMatch();
|
||||
} else {
|
||||
nameMatches = caseSensitive
|
||||
? symbolName == query
|
||||
: symbolName.compare(query, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
bool typeMatches = (symbolType == SymbolType::All)
|
||||
|| (symbolType == SymbolType::Class && symbol->asClass())
|
||||
|| (symbolType == SymbolType::Function && symbol->asFunction())
|
||||
|| (symbolType == SymbolType::Enum && symbol->asEnum())
|
||||
|| (symbolType == SymbolType::Variable && symbol->asDeclaration())
|
||||
|| (symbolType == SymbolType::Namespace && symbol->asNamespace());
|
||||
|
||||
if (nameMatches && typeMatches) {
|
||||
SearchResult result;
|
||||
result.filePath = filePath;
|
||||
|
||||
auto projects = ProjectExplorer::ProjectManager::projects();
|
||||
if (!projects.isEmpty()) {
|
||||
QString projectDir = projects.first()->projectDirectory().path();
|
||||
result.relativePath = QDir(projectDir).relativeFilePath(filePath);
|
||||
} else {
|
||||
result.relativePath = QFileInfo(filePath).fileName();
|
||||
}
|
||||
|
||||
result.content = symbolName;
|
||||
result.lineNumber = symbol->line();
|
||||
result.context = overview.prettyType(symbol->type());
|
||||
results.append(result);
|
||||
}
|
||||
|
||||
if (auto nestedScope = symbol->asScope()) {
|
||||
self(self, nestedScope);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
searchInScope(searchInScope, document->globalNamespace());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
ProjectSearchTool::SymbolType ProjectSearchTool::parseSymbolType(const QString &typeStr)
|
||||
{
|
||||
if (typeStr == "class")
|
||||
return SymbolType::Class;
|
||||
if (typeStr == "function")
|
||||
return SymbolType::Function;
|
||||
if (typeStr == "enum")
|
||||
return SymbolType::Enum;
|
||||
if (typeStr == "variable")
|
||||
return SymbolType::Variable;
|
||||
if (typeStr == "namespace")
|
||||
return SymbolType::Namespace;
|
||||
return SymbolType::All;
|
||||
}
|
||||
|
||||
QString ProjectSearchTool::formatResults(const QList<SearchResult> &results, const QString &query)
|
||||
{
|
||||
QString output = QString("Query: %1\n Found %2 matches:\n\n").arg(query).arg(results.size());
|
||||
int count = 0;
|
||||
for (const auto &r : results) {
|
||||
if (++count > 100) {
|
||||
output += QString("... and %1 more matches").arg(results.size() - 20);
|
||||
break;
|
||||
}
|
||||
output += QString("%1:%2: %3\n").arg(r.relativePath).arg(r.lineNumber).arg(r.content);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
57
sources/tools/ProjectSearchTool.hpp
Normal file
57
sources/tools/ProjectSearchTool.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <context/IgnoreManager.hpp>
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
#include <QFuture>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class ProjectSearchTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProjectSearchTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input) override;
|
||||
|
||||
private:
|
||||
enum class SearchType { Text, Symbol };
|
||||
enum class SymbolType { All, Class, Function, Enum, Variable, Namespace };
|
||||
|
||||
struct SearchResult
|
||||
{
|
||||
QString filePath;
|
||||
QString relativePath;
|
||||
QString content;
|
||||
int lineNumber = 0;
|
||||
QString context;
|
||||
};
|
||||
|
||||
QList<SearchResult> searchText(
|
||||
const QString &query,
|
||||
bool caseSensitive,
|
||||
bool useRegex,
|
||||
bool wholeWords,
|
||||
const QString &filePattern);
|
||||
|
||||
QList<SearchResult> searchSymbols(
|
||||
const QString &query, SymbolType symbolType, bool caseSensitive, bool useRegex);
|
||||
|
||||
SymbolType parseSymbolType(const QString &typeStr);
|
||||
QString formatResults(const QList<SearchResult> &results, const QString &query);
|
||||
|
||||
Context::IgnoreManager *m_ignoreManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
107
sources/tools/ReadFileTool.cpp
Normal file
107
sources/tools/ReadFileTool.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ReadFileTool.hpp"
|
||||
|
||||
#include "FileSearchUtils.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <context/ProjectUtils.hpp>
|
||||
#include <logger/Logger.hpp>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
ReadFileTool::ReadFileTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{}
|
||||
|
||||
QString ReadFileTool::id() const
|
||||
{
|
||||
return "read_file";
|
||||
}
|
||||
|
||||
QString ReadFileTool::displayName() const
|
||||
{
|
||||
return "Reading file";
|
||||
}
|
||||
|
||||
QString ReadFileTool::description() const
|
||||
{
|
||||
return "Read the text content of a file. Accepts either an absolute path or a path "
|
||||
"relative to the project root. Reading files outside the active project requires "
|
||||
"the 'Allow file access outside project' option in settings. Use `find_file` first "
|
||||
"if you only know a partial filename.";
|
||||
}
|
||||
|
||||
QJsonObject ReadFileTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
properties["file_path"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
"Absolute path (e.g. /path/to/file.cpp) or project-relative path (e.g. src/main.cpp). "
|
||||
"Relative paths are resolved against the root of the active project."}};
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{"file_path"};
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> ReadFileTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([input]() -> LLMQore::ToolResult {
|
||||
QString rawPath = input["file_path"].toString().trimmed();
|
||||
if (rawPath.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument(
|
||||
"'file_path' parameter is required and cannot be empty");
|
||||
}
|
||||
|
||||
QFileInfo pathInfo(rawPath);
|
||||
QString absolutePath;
|
||||
if (pathInfo.isAbsolute()) {
|
||||
absolutePath = rawPath;
|
||||
} else {
|
||||
QString projectRoot = Context::ProjectUtils::getProjectRoot();
|
||||
if (projectRoot.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Cannot resolve relative path '%1': no project is open. "
|
||||
"Provide an absolute path or open a project.")
|
||||
.arg(rawPath));
|
||||
}
|
||||
absolutePath = QDir(projectRoot).absoluteFilePath(rawPath);
|
||||
LOG_MESSAGE(QString("ReadFileTool: Resolved relative path '%1' to '%2'")
|
||||
.arg(rawPath, absolutePath));
|
||||
}
|
||||
|
||||
QFileInfo finalInfo(absolutePath);
|
||||
if (!finalInfo.exists() || !finalInfo.isFile()) {
|
||||
throw LLMQore::ToolRuntimeError(QString("File does not exist: %1").arg(absolutePath));
|
||||
}
|
||||
|
||||
QString content = FileSearchUtils::readFileContent(absolutePath);
|
||||
if (content.isNull()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Cannot read file '%1'. It may be outside the project scope "
|
||||
"(enable 'Allow file access outside project' in settings), unreadable, "
|
||||
"or use an unsupported encoding.")
|
||||
.arg(absolutePath));
|
||||
}
|
||||
|
||||
QString result = QString("File: %1\n\n%2").arg(absolutePath, content);
|
||||
return LLMQore::ToolResult::text(result);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
28
sources/tools/ReadFileTool.hpp
Normal file
28
sources/tools/ReadFileTool.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
#include <QFuture>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class ReadFileTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ReadFileTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input) override;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
197
sources/tools/ReadOriginalHistoryTool.cpp
Normal file
197
sources/tools/ReadOriginalHistoryTool.cpp
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ReadOriginalHistoryTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
namespace {
|
||||
|
||||
QString roleName(int role)
|
||||
{
|
||||
switch (role) {
|
||||
case 0:
|
||||
return QStringLiteral("system");
|
||||
case 1:
|
||||
return QStringLiteral("user");
|
||||
case 2:
|
||||
return QStringLiteral("assistant");
|
||||
case 3:
|
||||
return QStringLiteral("tool");
|
||||
case 4:
|
||||
return QStringLiteral("file_edit");
|
||||
case 5:
|
||||
return QStringLiteral("thinking");
|
||||
default:
|
||||
return QStringLiteral("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject readJsonObject(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
return doc.isObject() ? doc.object() : QJsonObject{};
|
||||
}
|
||||
|
||||
QString resolveRootHistoryPath(const QString &sessionPath)
|
||||
{
|
||||
QString current = sessionPath;
|
||||
QString rootPath;
|
||||
|
||||
for (int depth = 0; depth < 32; ++depth) {
|
||||
const QJsonObject obj = readJsonObject(current);
|
||||
const QString parent = obj.value("compressedFrom").toString();
|
||||
if (parent.isEmpty() || parent == current)
|
||||
break;
|
||||
if (!QFile::exists(parent))
|
||||
break;
|
||||
rootPath = parent;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return rootPath;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ReadOriginalHistoryTool::ReadOriginalHistoryTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{}
|
||||
|
||||
QString ReadOriginalHistoryTool::id() const
|
||||
{
|
||||
return "read_original_history";
|
||||
}
|
||||
|
||||
QString ReadOriginalHistoryTool::displayName() const
|
||||
{
|
||||
return "Reading pre-compression history";
|
||||
}
|
||||
|
||||
QString ReadOriginalHistoryTool::description() const
|
||||
{
|
||||
return "Read the original, full chat history from before this conversation was "
|
||||
"compressed into a summary. Use this only when the summary in context is "
|
||||
"missing a detail you need (an exact code snippet, file path, decision, or "
|
||||
"wording). The result can be large, so prefer the 'query' parameter to search "
|
||||
"and 'offset'/'limit' to page through messages. Returns nothing useful if the "
|
||||
"conversation was never compressed.";
|
||||
}
|
||||
|
||||
QJsonObject ReadOriginalHistoryTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
|
||||
properties["query"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
"Optional case-insensitive substring. When set, only messages whose content "
|
||||
"contains it are returned."}};
|
||||
|
||||
properties["role"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
"Optional role filter: 'user', 'assistant', 'system' or 'tool'."}};
|
||||
|
||||
properties["offset"] = QJsonObject{
|
||||
{"type", "integer"},
|
||||
{"description", "Index of the first matching message to return (default 0)."}};
|
||||
|
||||
properties["limit"] = QJsonObject{
|
||||
{"type", "integer"},
|
||||
{"description", "Maximum number of messages to return (default 20)."}};
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{};
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> ReadOriginalHistoryTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
QString sessionPath;
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
sessionPath = m_currentSessionId;
|
||||
}
|
||||
|
||||
return QtConcurrent::run([input, sessionPath]() -> LLMQore::ToolResult {
|
||||
if (sessionPath.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
"No active chat session, cannot locate pre-compression history.");
|
||||
}
|
||||
|
||||
const QString rootPath = resolveRootHistoryPath(sessionPath);
|
||||
if (rootPath.isEmpty()) {
|
||||
return LLMQore::ToolResult::text(
|
||||
"This conversation was never compressed; there is no separate "
|
||||
"pre-compression history. The messages already in context are the full "
|
||||
"history.");
|
||||
}
|
||||
|
||||
const QJsonObject root = readJsonObject(rootPath);
|
||||
const QJsonArray messages = root.value("messages").toArray();
|
||||
|
||||
const QString query = input.value("query").toString().trimmed();
|
||||
const QString roleFilter = input.value("role").toString().trimmed().toLower();
|
||||
const int offset = qMax(0, input.value("offset").toInt(0));
|
||||
const int limit = qBound(1, input.value("limit").toInt(20), 200);
|
||||
|
||||
QStringList matched;
|
||||
int matchCount = 0;
|
||||
for (int i = 0; i < messages.size(); ++i) {
|
||||
const QJsonObject msg = messages.at(i).toObject();
|
||||
const QString role = roleName(msg.value("role").toInt());
|
||||
const QString content = msg.value("content").toString();
|
||||
|
||||
if (!roleFilter.isEmpty() && role != roleFilter)
|
||||
continue;
|
||||
if (!query.isEmpty() && !content.contains(query, Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
++matchCount;
|
||||
if (matchCount <= offset || matched.size() >= limit)
|
||||
continue;
|
||||
|
||||
matched.append(QString("[#%1 %2]\n%3").arg(i).arg(role, content));
|
||||
}
|
||||
|
||||
const int shown = matched.size();
|
||||
QString header = QString("Pre-compression history (%1): %2 matching message(s)")
|
||||
.arg(rootPath)
|
||||
.arg(matchCount);
|
||||
if (shown < matchCount || offset > 0) {
|
||||
header += QString(", showing %1-%2")
|
||||
.arg(offset + 1)
|
||||
.arg(offset + shown);
|
||||
}
|
||||
|
||||
if (shown == 0)
|
||||
return LLMQore::ToolResult::text(header + "\n\nNo messages to display.");
|
||||
|
||||
return LLMQore::ToolResult::text(header + "\n\n" + matched.join("\n\n---\n\n"));
|
||||
});
|
||||
}
|
||||
|
||||
void ReadOriginalHistoryTool::setCurrentSessionId(const QString &sessionId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_currentSessionId = sessionId;
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
35
sources/tools/ReadOriginalHistoryTool.hpp
Normal file
35
sources/tools/ReadOriginalHistoryTool.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class ReadOriginalHistoryTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ReadOriginalHistoryTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
void setCurrentSessionId(const QString &sessionId);
|
||||
|
||||
private:
|
||||
mutable QMutex m_mutex;
|
||||
QString m_currentSessionId;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
85
sources/tools/SkillTool.cpp
Normal file
85
sources/tools/SkillTool.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "SkillTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "skills/AgentSkill.hpp"
|
||||
#include "skills/SkillsManager.hpp"
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
SkillTool::SkillTool(Skills::SkillsManager *skillsManager, QObject *parent)
|
||||
: BaseTool(parent)
|
||||
, m_skillsManager(skillsManager)
|
||||
{}
|
||||
|
||||
QString SkillTool::id() const
|
||||
{
|
||||
return "load_skill";
|
||||
}
|
||||
|
||||
QString SkillTool::displayName() const
|
||||
{
|
||||
return "Loading skill";
|
||||
}
|
||||
|
||||
QString SkillTool::description() const
|
||||
{
|
||||
return "Load the full instructions of a skill by name. The Available Skills catalog in "
|
||||
"the system prompt lists each skill's name and a short description. When a request "
|
||||
"matches a skill, call this tool with that skill's name to load its complete "
|
||||
"instructions, then follow them.";
|
||||
}
|
||||
|
||||
QJsonObject SkillTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject properties;
|
||||
properties["name"] = QJsonObject{
|
||||
{"type", "string"},
|
||||
{"description",
|
||||
"Exact name of the skill to load, as shown in the Available Skills catalog."}};
|
||||
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
definition["properties"] = properties;
|
||||
definition["required"] = QJsonArray{"name"};
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> SkillTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
const QString name = input["name"].toString().trimmed();
|
||||
|
||||
const std::optional<Skills::AgentSkill> found
|
||||
= m_skillsManager ? m_skillsManager->findByName(name) : std::nullopt;
|
||||
|
||||
return QtConcurrent::run([name, found]() -> LLMQore::ToolResult {
|
||||
if (name.isEmpty()) {
|
||||
throw LLMQore::ToolInvalidArgument(
|
||||
"'name' parameter is required and cannot be empty");
|
||||
}
|
||||
if (!found) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Unknown skill: '%1'. Use a skill name from the Available Skills "
|
||||
"catalog in the system prompt.")
|
||||
.arg(name));
|
||||
}
|
||||
if (found->body.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
QString("Skill '%1' has no instructions.").arg(found->name));
|
||||
}
|
||||
|
||||
return LLMQore::ToolResult::text(
|
||||
QString("Skill: %1\n\n%2").arg(found->name, found->body));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
36
sources/tools/SkillTool.hpp
Normal file
36
sources/tools/SkillTool.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
#include <QFuture>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class SkillTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SkillTool(Skills::SkillsManager *skillsManager, QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input) override;
|
||||
|
||||
private:
|
||||
QPointer<Skills::SkillsManager> m_skillsManager;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
335
sources/tools/TodoTool.cpp
Normal file
335
sources/tools/TodoTool.cpp
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "TodoTool.hpp"
|
||||
|
||||
#include <LLMQore/ToolExceptions.hpp>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QMutexLocker>
|
||||
#include <QtConcurrent>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
TodoTool::TodoTool(QObject *parent)
|
||||
: BaseTool(parent)
|
||||
{}
|
||||
|
||||
QString TodoTool::id() const
|
||||
{
|
||||
return "todo_tool";
|
||||
}
|
||||
|
||||
QString TodoTool::displayName() const
|
||||
{
|
||||
return "Managing TODO list for task tracking";
|
||||
}
|
||||
|
||||
QString TodoTool::description() const
|
||||
{
|
||||
return "Track and organize multi-step tasks during complex operations that require multiple "
|
||||
"sequential steps. "
|
||||
"**Use when planning 3+ step workflows.** "
|
||||
"Operations: 'add' - provide array of task descriptions to create full plan at once, "
|
||||
"'complete' - provide array of task IDs to mark finished steps, 'list' - review "
|
||||
"progress. "
|
||||
"Helpful for: large refactorings, feature implementations, debugging workflows. "
|
||||
"The list persists throughout the conversation.";
|
||||
}
|
||||
|
||||
QJsonObject TodoTool::parametersSchema() const
|
||||
{
|
||||
QJsonObject definition;
|
||||
definition["type"] = "object";
|
||||
|
||||
QJsonObject properties;
|
||||
|
||||
QJsonObject operationProp;
|
||||
operationProp["type"] = "string";
|
||||
operationProp["description"] = "Operation: 'add' (create tasks), 'complete' (mark tasks as "
|
||||
"done), 'list' (show all tasks)";
|
||||
QJsonArray operationEnum;
|
||||
operationEnum.append("add");
|
||||
operationEnum.append("complete");
|
||||
operationEnum.append("list");
|
||||
operationProp["enum"] = operationEnum;
|
||||
properties["operation"] = operationProp;
|
||||
|
||||
QJsonObject tasksProp;
|
||||
tasksProp["type"] = "array";
|
||||
QJsonObject tasksItems;
|
||||
tasksItems["type"] = "string";
|
||||
tasksProp["items"] = tasksItems;
|
||||
tasksProp["description"]
|
||||
= "Array of task descriptions to create (required for 'add' operation). "
|
||||
"Create all subtasks at once, e.g.: ['Step 1: ...', 'Step 2: ...', 'Step 3: ...']";
|
||||
properties["tasks"] = tasksProp;
|
||||
|
||||
QJsonObject todoIdsProp;
|
||||
todoIdsProp["type"] = "array";
|
||||
QJsonObject todoIdsItems;
|
||||
todoIdsItems["type"] = "integer";
|
||||
todoIdsProp["items"] = todoIdsItems;
|
||||
todoIdsProp["description"]
|
||||
= "Array of todo item IDs to mark as completed (required for 'complete' operation). "
|
||||
"Example: [1, 2, 5] to complete tasks #1, #2, and #5";
|
||||
properties["todo_ids"] = todoIdsProp;
|
||||
|
||||
definition["properties"] = properties;
|
||||
|
||||
QJsonArray required;
|
||||
required.append("operation");
|
||||
definition["required"] = required;
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
QFuture<LLMQore::ToolResult> TodoTool::executeAsync(const QJsonObject &input)
|
||||
{
|
||||
return QtConcurrent::run([this, input]() -> LLMQore::ToolResult {
|
||||
QMutexLocker sessionLocker(&m_mutex);
|
||||
QString sessionId = m_currentSessionId.isEmpty() ? "current" : m_currentSessionId;
|
||||
sessionLocker.unlock();
|
||||
|
||||
const QString operation = input.value("operation").toString();
|
||||
|
||||
if (operation == "add") {
|
||||
if (!input.contains("tasks") || !input.value("tasks").isArray()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
tr("Error: 'tasks' parameter (array) is required for 'add' operation. "
|
||||
"Example: {\"operation\": \"add\", \"tasks\": [\"Task 1\", \"Task 2\"]}"));
|
||||
}
|
||||
|
||||
const QJsonArray tasksArray = input.value("tasks").toArray();
|
||||
if (tasksArray.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
tr("Error: 'tasks' array cannot be empty. Provide at least one task."));
|
||||
}
|
||||
|
||||
QStringList tasks;
|
||||
for (const QJsonValue &taskValue : tasksArray) {
|
||||
QString task = taskValue.toString().trimmed();
|
||||
if (!task.isEmpty()) {
|
||||
tasks.append(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
tr("Error: All tasks in 'tasks' array are empty strings."));
|
||||
}
|
||||
|
||||
return LLMQore::ToolResult::text(addTodos(sessionId, tasks));
|
||||
|
||||
} else if (operation == "complete") {
|
||||
if (!input.contains("todo_ids") || !input.value("todo_ids").isArray()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
tr("Error: 'todo_ids' parameter (array) is required for 'complete' operation. "
|
||||
"Example: {\"operation\": \"complete\", \"todo_ids\": [1, 2, 3]}"));
|
||||
}
|
||||
|
||||
const QJsonArray idsArray = input.value("todo_ids").toArray();
|
||||
if (idsArray.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
tr("Error: 'todo_ids' array cannot be empty. Provide at least one ID."));
|
||||
}
|
||||
|
||||
QList<int> ids;
|
||||
for (const QJsonValue &idValue : idsArray) {
|
||||
int id = idValue.toInt(-1);
|
||||
if (id > 0) {
|
||||
ids.append(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.isEmpty()) {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
tr("Error: All IDs in 'todo_ids' array are invalid. IDs must be positive "
|
||||
"integers."));
|
||||
}
|
||||
|
||||
return LLMQore::ToolResult::text(completeTodos(sessionId, ids));
|
||||
|
||||
} else if (operation == "list") {
|
||||
return LLMQore::ToolResult::text(listTodos(sessionId));
|
||||
|
||||
} else {
|
||||
throw LLMQore::ToolRuntimeError(
|
||||
tr("Error: Unknown operation '%1'. Valid operations: 'add', 'complete', 'list'")
|
||||
.arg(operation));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void TodoTool::setCurrentSessionId(const QString &sessionId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_currentSessionId = sessionId;
|
||||
}
|
||||
|
||||
void TodoTool::clearSession(const QString &sessionId)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_sessionTodos.remove(sessionId);
|
||||
m_sessionNextId.remove(sessionId);
|
||||
}
|
||||
|
||||
QString TodoTool::addTodos(const QString &sessionId, const QStringList &tasks)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
if (!m_sessionTodos.contains(sessionId)) {
|
||||
m_sessionTodos[sessionId] = QHash<int, TodoItem>();
|
||||
m_sessionNextId[sessionId] = 1;
|
||||
}
|
||||
|
||||
for (const QString &task : tasks) {
|
||||
const int newId = m_sessionNextId[sessionId]++;
|
||||
m_sessionTodos[sessionId][newId] = {newId, task, false};
|
||||
}
|
||||
|
||||
const QString summary = (tasks.size() == 1) ? tr("✓ Added 1 new task")
|
||||
: tr("✓ Added %1 new tasks").arg(tasks.size());
|
||||
|
||||
return QString("%1\n\n%2").arg(summary, listTodosLocked(sessionId));
|
||||
}
|
||||
|
||||
QString TodoTool::completeTodos(const QString &sessionId, const QList<int> &todoIds)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
|
||||
if (!m_sessionTodos.contains(sessionId)) {
|
||||
throw LLMQore::ToolRuntimeError(tr("Error: No todos found in this session"));
|
||||
}
|
||||
|
||||
auto &todos = m_sessionTodos[sessionId];
|
||||
int completedCount = 0;
|
||||
int alreadyCompletedCount = 0;
|
||||
QStringList notFound;
|
||||
|
||||
for (const int todoId : todoIds) {
|
||||
if (!todos.contains(todoId)) {
|
||||
notFound.append(QString("#%1").arg(todoId));
|
||||
continue;
|
||||
}
|
||||
|
||||
TodoItem &item = todos[todoId];
|
||||
if (item.completed) {
|
||||
alreadyCompletedCount++;
|
||||
} else {
|
||||
item.completed = true;
|
||||
completedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
QStringList messages;
|
||||
if (completedCount > 0) {
|
||||
messages.append((completedCount == 1) ? tr("✓ Marked 1 task as completed")
|
||||
: tr("✓ Marked %1 tasks as completed")
|
||||
.arg(completedCount));
|
||||
}
|
||||
|
||||
if (alreadyCompletedCount > 0) {
|
||||
messages.append(tr("⚠ %1 already completed").arg(alreadyCompletedCount));
|
||||
}
|
||||
|
||||
if (!notFound.isEmpty()) {
|
||||
messages.append(tr("❌ Not found: %1").arg(notFound.join(", ")));
|
||||
}
|
||||
|
||||
const QString summary = messages.join(", ");
|
||||
return QString("%1\n\n%2").arg(summary, listRemainingTodosLocked(sessionId));
|
||||
}
|
||||
|
||||
QString TodoTool::listTodos(const QString &sessionId) const
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
return listTodosLocked(sessionId);
|
||||
}
|
||||
|
||||
QString TodoTool::listTodosLocked(const QString &sessionId) const
|
||||
{
|
||||
const auto it = m_sessionTodos.constFind(sessionId);
|
||||
if (it == m_sessionTodos.constEnd() || it->isEmpty()) {
|
||||
return tr("📋 TODO List: (empty)");
|
||||
}
|
||||
|
||||
const auto &todos = *it;
|
||||
QList<int> ids = todos.keys();
|
||||
std::sort(ids.begin(), ids.end());
|
||||
|
||||
QStringList lines;
|
||||
lines.reserve(ids.size() + 4);
|
||||
lines.append(tr("📋 TODO List:"));
|
||||
lines.append("");
|
||||
|
||||
int completedCount = 0;
|
||||
for (const int id : ids) {
|
||||
const TodoItem &item = todos[id];
|
||||
const QString checkbox = item.completed ? "[x]" : "[ ]";
|
||||
const QString strikethrough = item.completed ? QString("~~") : QString("");
|
||||
|
||||
lines.append(QString("%1 **#%2** %3%4%5")
|
||||
.arg(checkbox)
|
||||
.arg(id)
|
||||
.arg(strikethrough, item.task, strikethrough));
|
||||
|
||||
if (item.completed) {
|
||||
completedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
lines.append("");
|
||||
const int totalCount = ids.size();
|
||||
const int percentage = totalCount > 0 ? (completedCount * 100) / totalCount : 0;
|
||||
lines.append(
|
||||
tr("Progress: %1/%2 completed (%3%)").arg(completedCount).arg(totalCount).arg(percentage));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
QString TodoTool::listRemainingTodosLocked(const QString &sessionId) const
|
||||
{
|
||||
const auto it = m_sessionTodos.constFind(sessionId);
|
||||
if (it == m_sessionTodos.constEnd() || it->isEmpty()) {
|
||||
return tr("📋 All tasks completed! 🎉");
|
||||
}
|
||||
|
||||
const auto &todos = *it;
|
||||
QList<int> ids = todos.keys();
|
||||
std::sort(ids.begin(), ids.end());
|
||||
|
||||
int completedCount = 0;
|
||||
QStringList remainingLines;
|
||||
|
||||
for (const int id : ids) {
|
||||
const TodoItem &item = todos[id];
|
||||
if (item.completed) {
|
||||
completedCount++;
|
||||
} else {
|
||||
remainingLines.append(QString("[ ] **#%1** %2").arg(id).arg(item.task));
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingLines.isEmpty()) {
|
||||
return tr("📋 All tasks completed! 🎉");
|
||||
}
|
||||
|
||||
QStringList lines;
|
||||
lines.reserve(remainingLines.size() + 4);
|
||||
lines.append(tr("📋 Remaining tasks:"));
|
||||
lines.append("");
|
||||
lines.append(remainingLines);
|
||||
lines.append("");
|
||||
|
||||
const int totalCount = ids.size();
|
||||
const int percentage = totalCount > 0 ? (completedCount * 100) / totalCount : 0;
|
||||
lines.append(
|
||||
tr("Progress: %1/%2 completed (%3%)").arg(completedCount).arg(totalCount).arg(percentage));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
52
sources/tools/TodoTool.hpp
Normal file
52
sources/tools/TodoTool.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LLMQore/BaseTool.hpp>
|
||||
|
||||
#include <QHash>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
struct TodoItem
|
||||
{
|
||||
int id;
|
||||
QString task;
|
||||
bool completed;
|
||||
};
|
||||
|
||||
class TodoTool : public ::LLMQore::BaseTool
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TodoTool(QObject *parent = nullptr);
|
||||
|
||||
QString id() const override;
|
||||
QString displayName() const override;
|
||||
QString description() const override;
|
||||
QJsonObject parametersSchema() const override;
|
||||
|
||||
QFuture<LLMQore::ToolResult> executeAsync(const QJsonObject &input = QJsonObject()) override;
|
||||
|
||||
void setCurrentSessionId(const QString &sessionId);
|
||||
void clearSession(const QString &sessionId);
|
||||
|
||||
private:
|
||||
QString addTodos(const QString &sessionId, const QStringList &tasks);
|
||||
QString completeTodos(const QString &sessionId, const QList<int> &todoIds);
|
||||
QString listTodos(const QString &sessionId) const;
|
||||
QString listTodosLocked(const QString &sessionId) const;
|
||||
QString listRemainingTodosLocked(const QString &sessionId) const;
|
||||
|
||||
mutable QMutex m_mutex;
|
||||
QString m_currentSessionId;
|
||||
QHash<QString, QHash<int, TodoItem>> m_sessionTodos;
|
||||
QHash<QString, int> m_sessionNextId;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
54
sources/tools/ToolExceptions.hpp
Normal file
54
sources/tools/ToolExceptions.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QException>
|
||||
#include <QString>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
class ToolException : public QException
|
||||
{
|
||||
public:
|
||||
explicit ToolException(const QString &message)
|
||||
: m_message(message)
|
||||
, m_stdMessage(message.toStdString())
|
||||
{}
|
||||
|
||||
void raise() const override { throw *this; }
|
||||
ToolException *clone() const override { return new ToolException(*this); }
|
||||
const char *what() const noexcept override { return m_stdMessage.c_str(); }
|
||||
|
||||
QString message() const { return m_message; }
|
||||
|
||||
private:
|
||||
QString m_message;
|
||||
std::string m_stdMessage;
|
||||
};
|
||||
|
||||
class ToolRuntimeError : public ToolException
|
||||
{
|
||||
public:
|
||||
explicit ToolRuntimeError(const QString &message)
|
||||
: ToolException(message)
|
||||
{}
|
||||
|
||||
void raise() const override { throw *this; }
|
||||
ToolRuntimeError *clone() const override { return new ToolRuntimeError(*this); }
|
||||
};
|
||||
|
||||
class ToolInvalidArgument : public ToolException
|
||||
{
|
||||
public:
|
||||
explicit ToolInvalidArgument(const QString &message)
|
||||
: ToolException(message)
|
||||
{}
|
||||
|
||||
void raise() const override { throw *this; }
|
||||
ToolInvalidArgument *clone() const override { return new ToolInvalidArgument(*this); }
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
93
sources/tools/ToolsRegistration.cpp
Normal file
93
sources/tools/ToolsRegistration.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright (C) 2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ToolsRegistration.hpp"
|
||||
|
||||
#include <LLMQore/ToolsManager.hpp>
|
||||
|
||||
#include <settings/ToolsSettings.hpp>
|
||||
#include <utils/aspects.h>
|
||||
|
||||
#include "BuildProjectTool.hpp"
|
||||
#include "CreateNewFileTool.hpp"
|
||||
#include "EditFileTool.hpp"
|
||||
#include "ExecuteTerminalCommandTool.hpp"
|
||||
#include "FindFileTool.hpp"
|
||||
#include "GetIssuesListTool.hpp"
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
#include "ProjectSearchTool.hpp"
|
||||
#include "ReadFileTool.hpp"
|
||||
#include "ReadOriginalHistoryTool.hpp"
|
||||
#include "SkillTool.hpp"
|
||||
#include "TodoTool.hpp"
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
namespace {
|
||||
|
||||
template<typename ToolT>
|
||||
void wireTool(::LLMQore::ToolsManager *manager,
|
||||
Utils::BoolAspect &aspect,
|
||||
const QString &toolId)
|
||||
{
|
||||
auto sync = [manager, toolId, &aspect]() {
|
||||
const bool wanted = aspect.volatileValue();
|
||||
const bool present = manager->tool(toolId) != nullptr;
|
||||
if (wanted && !present) {
|
||||
manager->addTool(new ToolT(manager));
|
||||
} else if (!wanted && present) {
|
||||
manager->removeTool(toolId);
|
||||
}
|
||||
};
|
||||
|
||||
sync();
|
||||
|
||||
QObject::connect(&aspect, &Utils::BoolAspect::volatileValueChanged, manager, sync);
|
||||
QObject::connect(&aspect, &Utils::BaseAspect::changed, manager, sync);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void registerQodeAssistTools(::LLMQore::ToolsManager *manager)
|
||||
{
|
||||
auto &s = Settings::toolsSettings();
|
||||
|
||||
wireTool<ListProjectFilesTool>(manager, s.enableListProjectFilesTool, "list_project_files");
|
||||
wireTool<FindFileTool>(manager, s.enableFindFileTool, "find_file");
|
||||
wireTool<ReadFileTool>(manager, s.enableReadFileTool, "read_file");
|
||||
wireTool<ProjectSearchTool>(manager, s.enableProjectSearchTool, "search_project");
|
||||
wireTool<CreateNewFileTool>(manager, s.enableCreateNewFileTool, "create_new_file");
|
||||
wireTool<EditFileTool>(manager, s.enableEditFileTool, "edit_file");
|
||||
wireTool<BuildProjectTool>(manager, s.enableBuildProjectTool, "build_project");
|
||||
wireTool<GetIssuesListTool>(manager, s.enableGetIssuesListTool, "get_issues_list");
|
||||
wireTool<ExecuteTerminalCommandTool>(
|
||||
manager, s.enableTerminalCommandTool, "execute_terminal_command");
|
||||
wireTool<TodoTool>(manager, s.enableTodoTool, "todo_tool");
|
||||
wireTool<ReadOriginalHistoryTool>(
|
||||
manager, s.enableReadOriginalHistoryTool, "read_original_history");
|
||||
}
|
||||
|
||||
void registerSkillTool(
|
||||
::LLMQore::ToolsManager *manager, Skills::SkillsManager *skillsManager)
|
||||
{
|
||||
Utils::BoolAspect &aspect = Settings::toolsSettings().enableSkillTool;
|
||||
const QString toolId = QStringLiteral("load_skill");
|
||||
|
||||
auto sync = [manager, toolId, &aspect, skillsManager]() {
|
||||
const bool wanted = aspect.volatileValue();
|
||||
const bool present = manager->tool(toolId) != nullptr;
|
||||
if (wanted && !present) {
|
||||
manager->addTool(new SkillTool(skillsManager, manager));
|
||||
} else if (!wanted && present) {
|
||||
manager->removeTool(toolId);
|
||||
}
|
||||
};
|
||||
|
||||
sync();
|
||||
|
||||
QObject::connect(&aspect, &Utils::BoolAspect::volatileValueChanged, manager, sync);
|
||||
QObject::connect(&aspect, &Utils::BaseAspect::changed, manager, sync);
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
22
sources/tools/ToolsRegistration.hpp
Normal file
22
sources/tools/ToolsRegistration.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2025-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace LLMQore {
|
||||
class ToolsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Skills {
|
||||
class SkillsManager;
|
||||
}
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
void registerQodeAssistTools(::LLMQore::ToolsManager *manager);
|
||||
|
||||
void registerSkillTool(
|
||||
::LLMQore::ToolsManager *manager, Skills::SkillsManager *skillsManager);
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
Reference in New Issue
Block a user