diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e3f2b2..48faf02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,6 +154,7 @@ add_qtc_plugin(QodeAssist tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp + tools/TodoTool.hpp tools/TodoTool.cpp providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp providers/OllamaMessage.hpp providers/OllamaMessage.cpp diff --git a/ChatView/ChatRootView.cpp b/ChatView/ChatRootView.cpp index e62ea86..ad04317 100644 --- a/ChatView/ChatRootView.cpp +++ b/ChatView/ChatRootView.cpp @@ -311,6 +311,12 @@ void ChatRootView::clearLinkedFiles() emit linkedFilesChanged(); } +void ChatRootView::clearMessages() +{ + m_clientInterface->clearMessages(); + clearLinkedFiles(); +} + QString ChatRootView::getChatsHistoryDir() const { QString path; diff --git a/ChatView/ChatRootView.hpp b/ChatView/ChatRootView.hpp index c7d9ef9..a254fbe 100644 --- a/ChatView/ChatRootView.hpp +++ b/ChatView/ChatRootView.hpp @@ -161,6 +161,7 @@ public slots: void cancelRequest(); void clearAttachmentFiles(); void clearLinkedFiles(); + void clearMessages(); signals: void chatModelChanged(); diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 2924c52..2ae8731 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -318,12 +318,23 @@ void ClientInterface::sendMessage( Qt::UniqueConnection); provider->sendRequest(requestId, config.url, config.providerRequest); + + if (provider->supportsTools() && provider->toolsManager()) { + provider->toolsManager()->setCurrentSessionId(m_chatFilePath); + } } void ClientInterface::clearMessages() { + const auto providerName = Settings::generalSettings().caProvider(); + auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); + + if (provider && !m_chatFilePath.isEmpty() && provider->supportsTools() + && provider->toolsManager()) { + provider->toolsManager()->clearTodoSession(m_chatFilePath); + } + m_chatModel->clear(); - LOG_MESSAGE("Chat history cleared"); } void ClientInterface::cancelRequest() @@ -596,6 +607,15 @@ QVector ClientInterface::loadImagesFromStorage( void ClientInterface::setChatFilePath(const QString &filePath) { + if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) { + const auto providerName = Settings::generalSettings().caProvider(); + auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); + + if (provider && provider->supportsTools() && provider->toolsManager()) { + provider->toolsManager()->clearTodoSession(m_chatFilePath); + } + } + m_chatFilePath = filePath; m_chatModel->setChatFilePath(filePath); } diff --git a/ChatView/qml/RootItem.qml b/ChatView/qml/RootItem.qml index c70b549..7faab15 100644 --- a/ChatView/qml/RootItem.qml +++ b/ChatView/qml/RootItem.qml @@ -446,7 +446,7 @@ ChatRootView { } function clearChat() { - root.chatModel.clear() + root.clearMessages() root.clearAttachmentFiles() root.updateInputTokensCount() } diff --git a/llmcore/IToolsManager.hpp b/llmcore/IToolsManager.hpp new file mode 100644 index 0000000..0637773 --- /dev/null +++ b/llmcore/IToolsManager.hpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include "BaseTool.hpp" + +namespace QodeAssist::LLMCore { + +class IToolsManager +{ +public: + virtual ~IToolsManager() = default; + + virtual void executeToolCall( + const QString &requestId, + const QString &toolId, + const QString &toolName, + const QJsonObject &input) = 0; + + virtual QJsonArray getToolsDefinitions( + ToolSchemaFormat format, + RunToolsFilter filter = RunToolsFilter::ALL) const = 0; + + virtual void cleanupRequest(const QString &requestId) = 0; + virtual void setCurrentSessionId(const QString &sessionId) = 0; + virtual void clearTodoSession(const QString &sessionId) = 0; +}; + +} // namespace QodeAssist::LLMCore diff --git a/llmcore/Provider.hpp b/llmcore/Provider.hpp index 5e50a9b..993a064 100644 --- a/llmcore/Provider.hpp +++ b/llmcore/Provider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -27,6 +27,7 @@ #include "ContextData.hpp" #include "DataBuffers.hpp" #include "HttpClient.hpp" +#include "IToolsManager.hpp" #include "PromptTemplate.hpp" #include "RequestType.hpp" @@ -71,6 +72,8 @@ public: virtual void cancelRequest(const RequestID &requestId); + virtual IToolsManager *toolsManager() const { return nullptr; } + HttpClient *httpClient() const; public slots: diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp index 4aca5c8..85ef88c 100644 --- a/providers/ClaudeProvider.cpp +++ b/providers/ClaudeProvider.cpp @@ -268,6 +268,11 @@ void ClaudeProvider::cancelRequest(const LLMCore::RequestID &requestId) cleanupRequest(requestId); } +LLMCore::IToolsManager *ClaudeProvider::toolsManager() const +{ + return m_toolsManager; +} + void ClaudeProvider::onDataReceived( const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) { @@ -531,6 +536,7 @@ void ClaudeProvider::handleMessageComplete(const QString &requestId) for (auto toolContent : toolUseContent) { auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); + m_toolsManager->executeToolCall( requestId, toolContent->id(), toolContent->name(), toolContent->input()); } diff --git a/providers/ClaudeProvider.hpp b/providers/ClaudeProvider.hpp index 3802433..2796244 100644 --- a/providers/ClaudeProvider.hpp +++ b/providers/ClaudeProvider.hpp @@ -57,6 +57,8 @@ public: bool supportThinking() const override; bool supportImage() const override; void cancelRequest(const LLMCore::RequestID &requestId) override; + + LLMCore::IToolsManager *toolsManager() const override; public slots: void onDataReceived( diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 8d607a7..d51c25a 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -111,6 +111,7 @@ const char CA_ALLOW_ACCESS_OUTSIDE_PROJECT[] = "QodeAssist.caAllowAccessOutsideP const char CA_ENABLE_EDIT_FILE_TOOL[] = "QodeAssist.caEnableEditFileTool"; const char CA_ENABLE_BUILD_PROJECT_TOOL[] = "QodeAssist.caEnableBuildProjectTool"; const char CA_ENABLE_TERMINAL_COMMAND_TOOL[] = "QodeAssist.caEnableTerminalCommandTool"; +const char CA_ENABLE_TODO_TOOL[] = "QodeAssist.caEnableTodoTool"; const char CA_ALLOWED_TERMINAL_COMMANDS[] = "QodeAssist.caAllowedTerminalCommands"; const char CA_ALLOWED_TERMINAL_COMMANDS_LINUX[] = "QodeAssist.caAllowedTerminalCommandsLinux"; const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS"; diff --git a/settings/ToolsSettings.cpp b/settings/ToolsSettings.cpp index bb7dd13..afd9d6e 100644 --- a/settings/ToolsSettings.cpp +++ b/settings/ToolsSettings.cpp @@ -97,6 +97,13 @@ ToolsSettings::ToolsSettings() "unexpected behavior.")); enableTerminalCommandTool.setDefaultValue(false); + enableTodoTool.setSettingsKey(Constants::CA_ENABLE_TODO_TOOL); + enableTodoTool.setLabelText(Tr::tr("Enable Todo Tool")); + enableTodoTool.setToolTip( + Tr::tr("Enable the todo_tool that helps AI track and organize multi-step tasks. " + "Useful for complex refactoring, debugging, and feature implementation workflows.")); + enableTodoTool.setDefaultValue(true); + allowedTerminalCommandsLinux.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_LINUX); allowedTerminalCommandsLinux.setLabelText(Tr::tr("Allowed Commands (Linux)")); allowedTerminalCommandsLinux.setToolTip( @@ -158,6 +165,7 @@ ToolsSettings::ToolsSettings() enableEditFileTool, enableBuildProjectTool, enableTerminalCommandTool, + enableTodoTool, currentOsCommands, autoApplyFileEdits}}, Stretch{1}}; @@ -191,6 +199,7 @@ void ToolsSettings::resetSettingsToDefaults() resetAspect(enableEditFileTool); resetAspect(enableBuildProjectTool); resetAspect(enableTerminalCommandTool); + resetAspect(enableTodoTool); resetAspect(allowedTerminalCommandsLinux); resetAspect(allowedTerminalCommandsMacOS); resetAspect(allowedTerminalCommandsWindows); diff --git a/settings/ToolsSettings.hpp b/settings/ToolsSettings.hpp index 6a3b907..e96990a 100644 --- a/settings/ToolsSettings.hpp +++ b/settings/ToolsSettings.hpp @@ -41,6 +41,7 @@ public: Utils::BoolAspect enableEditFileTool{this}; Utils::BoolAspect enableBuildProjectTool{this}; Utils::BoolAspect enableTerminalCommandTool{this}; + Utils::BoolAspect enableTodoTool{this}; Utils::StringAspect allowedTerminalCommandsLinux{this}; Utils::StringAspect allowedTerminalCommandsMacOS{this}; Utils::StringAspect allowedTerminalCommandsWindows{this}; diff --git a/tools/TodoTool.cpp b/tools/TodoTool.cpp new file mode 100644 index 0000000..e58bcea --- /dev/null +++ b/tools/TodoTool.cpp @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2024-2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "TodoTool.hpp" +#include "ToolExceptions.hpp" + +#include +#include +#include +#include + +namespace QodeAssist::Tools { + +TodoTool::TodoTool(QObject *parent) + : BaseTool(parent) +{} + +QString TodoTool::name() const +{ + return "todo_tool"; +} + +QString TodoTool::stringName() 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::getDefinition(LLMCore::ToolSchemaFormat format) 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; + + switch (format) { + case LLMCore::ToolSchemaFormat::OpenAI: + return customizeForOpenAI(definition); + case LLMCore::ToolSchemaFormat::Claude: + return customizeForClaude(definition); + case LLMCore::ToolSchemaFormat::Ollama: + return customizeForOllama(definition); + case LLMCore::ToolSchemaFormat::Google: + return customizeForGoogle(definition); + } + + return definition; +} + +LLMCore::ToolPermissions TodoTool::requiredPermissions() const +{ + return LLMCore::ToolPermission::None; +} + +QFuture TodoTool::executeAsync(const QJsonObject &input) +{ + return QtConcurrent::run([this, input]() -> QString { + QString sessionId = input.value("session_id").toString(); + if (sessionId.isEmpty()) { + sessionId = "current"; + } + + const QString operation = input.value("operation").toString(); + + if (operation == "add") { + if (!input.contains("tasks") || !input.value("tasks").isArray()) { + throw 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 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 ToolRuntimeError( + tr("Error: All tasks in 'tasks' array are empty strings.")); + } + + return addTodos(sessionId, tasks); + + } else if (operation == "complete") { + if (!input.contains("todo_ids") || !input.value("todo_ids").isArray()) { + throw 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 ToolRuntimeError( + tr("Error: 'todo_ids' array cannot be empty. Provide at least one ID.")); + } + + QList ids; + for (const QJsonValue &idValue : idsArray) { + int id = idValue.toInt(-1); + if (id > 0) { + ids.append(id); + } + } + + if (ids.isEmpty()) { + throw ToolRuntimeError( + tr("Error: All IDs in 'todo_ids' array are invalid. IDs must be positive " + "integers.")); + } + + return completeTodos(sessionId, ids); + + } else if (operation == "list") { + return listTodos(sessionId); + + } else { + throw ToolRuntimeError( + tr("Error: Unknown operation '%1'. Valid operations: 'add', 'complete', 'list'") + .arg(operation)); + } + }); +} + +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(); + 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 &todoIds) +{ + QMutexLocker locker(&m_mutex); + + if (!m_sessionTodos.contains(sessionId)) { + throw 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 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 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 diff --git a/tools/TodoTool.hpp b/tools/TodoTool.hpp new file mode 100644 index 0000000..2454f82 --- /dev/null +++ b/tools/TodoTool.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024-2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include + +#include +#include +#include + +namespace QodeAssist::Tools { + +struct TodoItem +{ + int id; + QString task; + bool completed; +}; + +class TodoTool : public LLMCore::BaseTool +{ + Q_OBJECT + +public: + explicit TodoTool(QObject *parent = nullptr); + + QString name() const override; + QString stringName() const override; + QString description() const override; + QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override; + LLMCore::ToolPermissions requiredPermissions() const override; + + QFuture executeAsync(const QJsonObject &input = QJsonObject()) override; + + void clearSession(const QString &sessionId); + +private: + QString addTodos(const QString &sessionId, const QStringList &tasks); + QString completeTodos(const QString &sessionId, const QList &todoIds); + QString listTodos(const QString &sessionId) const; + QString listTodosLocked(const QString &sessionId) const; + QString listRemainingTodosLocked(const QString &sessionId) const; + + mutable QMutex m_mutex; + QHash> m_sessionTodos; + QHash m_sessionNextId; +}; + +} // namespace QodeAssist::Tools diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp index 4fdcee6..2d4bfe7 100644 --- a/tools/ToolsFactory.cpp +++ b/tools/ToolsFactory.cpp @@ -34,6 +34,7 @@ #include "ListProjectFilesTool.hpp" #include "ProjectSearchTool.hpp" #include "ReadVisibleFilesTool.hpp" +#include "TodoTool.hpp" namespace QodeAssist::Tools { @@ -54,6 +55,7 @@ void ToolsFactory::registerTools() registerTool(new ExecuteTerminalCommandTool(this)); registerTool(new ProjectSearchTool(this)); registerTool(new FindAndReadFileTool(this)); + registerTool(new TodoTool(this)); LOG_MESSAGE(QString("Registered %1 tools").arg(m_tools.size())); } @@ -107,6 +109,10 @@ QJsonArray ToolsFactory::getToolsDefinitions( continue; } + if (it.value()->name() == "todo_tool" && !settings.enableTodoTool()) { + continue; + } + const auto requiredPerms = it.value()->requiredPermissions(); if (filter != LLMCore::RunToolsFilter::ALL) { diff --git a/tools/ToolsManager.cpp b/tools/ToolsManager.cpp index 4c74169..ef7f605 100644 --- a/tools/ToolsManager.cpp +++ b/tools/ToolsManager.cpp @@ -18,6 +18,7 @@ */ #include "ToolsManager.hpp" +#include "TodoTool.hpp" #include "logger/Logger.hpp" namespace QodeAssist::Tools { @@ -74,6 +75,10 @@ void ToolsManager::executeToolCall( QJsonObject modifiedInput = input; modifiedInput["_request_id"] = requestId; + + if (!m_currentSessionId.isEmpty()) { + modifiedInput["session_id"] = m_currentSessionId; + } PendingTool pendingTool{toolId, toolName, modifiedInput, "", false}; queue.queue.append(pendingTool); @@ -140,27 +145,21 @@ QJsonArray ToolsManager::getToolsDefinitions( void ToolsManager::cleanupRequest(const QString &requestId) { if (m_toolQueues.contains(requestId)) { - LOG_MESSAGE(QString("ToolsManager: Canceling pending tools for request %1").arg(requestId)); m_toolHandler->cleanupRequest(requestId); m_toolQueues.remove(requestId); } - - LOG_MESSAGE(QString("ToolsManager: Cleaned up request %1").arg(requestId)); } void ToolsManager::onToolFinished( const QString &requestId, const QString &toolId, const QString &result, bool success) { if (!m_toolQueues.contains(requestId)) { - LOG_MESSAGE(QString("ToolsManager: Tool result for unknown request %1").arg(requestId)); return; } auto &queue = m_toolQueues[requestId]; if (!queue.completed.contains(toolId)) { - LOG_MESSAGE(QString("ToolsManager: Tool result for unknown tool %1 in request %2") - .arg(toolId, requestId)); return; } @@ -197,4 +196,17 @@ QHash ToolsManager::getToolResults(const QString &requestId) c return results; } +void ToolsManager::clearTodoSession(const QString &sessionId) +{ + auto *todoTool = qobject_cast(m_toolsFactory->getToolByName("todo_tool")); + if (todoTool) { + todoTool->clearSession(sessionId); + } +} + +void ToolsManager::setCurrentSessionId(const QString &sessionId) +{ + m_currentSessionId = sessionId; +} + } // namespace QodeAssist::Tools diff --git a/tools/ToolsManager.hpp b/tools/ToolsManager.hpp index 242b7e0..381c48d 100644 --- a/tools/ToolsManager.hpp +++ b/tools/ToolsManager.hpp @@ -27,6 +27,7 @@ #include "ToolHandler.hpp" #include "ToolsFactory.hpp" #include +#include namespace QodeAssist::Tools { @@ -46,7 +47,7 @@ struct ToolQueue bool isExecuting = false; }; -class ToolsManager : public QObject +class ToolsManager : public QObject, public LLMCore::IToolsManager { Q_OBJECT @@ -57,12 +58,15 @@ public: const QString &requestId, const QString &toolId, const QString &toolName, - const QJsonObject &input); + const QJsonObject &input) override; QJsonArray getToolsDefinitions( LLMCore::ToolSchemaFormat format, - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL) const; - void cleanupRequest(const QString &requestId); + LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL) const override; + + void cleanupRequest(const QString &requestId) override; + void setCurrentSessionId(const QString &sessionId) override; + void clearTodoSession(const QString &sessionId) override; ToolsFactory *toolsFactory() const; @@ -77,6 +81,7 @@ private: ToolsFactory *m_toolsFactory; ToolHandler *m_toolHandler; QHash m_toolQueues; + QString m_currentSessionId; void executeNextTool(const QString &requestId); QHash getToolResults(const QString &requestId) const;