From b33a1c2d43fde9cc47e5a1357baaa0fdc2eb397c Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Thu, 21 May 2026 14:19:16 +0200 Subject: [PATCH] fix: Add handling final argument for OpenAI responses tool calling --- ChatView/ClientInterface.cpp | 15 +++++++++---- settings/ToolsSettings.cpp | 11 +++++++--- sources/external/llmqore | 2 +- tools/CreateNewFileTool.cpp | 16 +++++++++++--- tools/EditFileTool.cpp | 16 +++++++++++--- tools/ExecuteTerminalCommandTool.cpp | 32 ++++++++++++++++++++++------ 6 files changed, 72 insertions(+), 20 deletions(-) diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index cdc19c1..4353315 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -174,14 +174,21 @@ void ClientInterface::sendMessage( auto project = PluginLLMCore::RulesLoader::getActiveProject(); if (project) { - systemPrompt += QString("\n# Active project name: %1").arg(project->displayName()); - systemPrompt += QString("\n# Active Project path: %1") + systemPrompt += QString("\n# Active project: %1").arg(project->displayName()); + systemPrompt += QString( + "\n# Project source root: %1" + "\n# All new source files, headers, QML and CMake edits MUST be " + "created or modified under this directory. Use absolute paths " + "rooted here, or project-relative paths.") .arg(project->projectDirectory().toUrlishString()); if (auto target = project->activeTarget()) { if (auto buildConfig = target->activeBuildConfiguration()) { - systemPrompt += QString("\n# Active Build directory: %1") - .arg(buildConfig->buildDirectory().toUrlishString()); + systemPrompt + += QString( + "\n# Build output directory (compiler artifacts only — do NOT " + "create or edit source files here): %1") + .arg(buildConfig->buildDirectory().toUrlishString()); } } diff --git a/settings/ToolsSettings.cpp b/settings/ToolsSettings.cpp index 7619502..a360c16 100644 --- a/settings/ToolsSettings.cpp +++ b/settings/ToolsSettings.cpp @@ -133,7 +133,9 @@ ToolsSettings::ToolsSettings() Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Linux. " "Example: git, ls, cat, grep, find, cmake")); allowedTerminalCommandsLinux.setDisplayStyle(Utils::StringAspect::LineEditDisplay); - allowedTerminalCommandsLinux.setDefaultValue("git, ls, cat, grep, find"); + allowedTerminalCommandsLinux.setDefaultValue( + "git, ls, cat, grep, find, pwd, echo, head, tail, wc, which, file, stat, tree, uname, " + "date, whoami, hostname"); allowedTerminalCommandsMacOS.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_MACOS); allowedTerminalCommandsMacOS.setLabelText(Tr::tr("Allowed Commands (macOS)")); @@ -141,7 +143,9 @@ ToolsSettings::ToolsSettings() Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on macOS. " "Example: git, ls, cat, grep, find, cmake")); allowedTerminalCommandsMacOS.setDisplayStyle(Utils::StringAspect::LineEditDisplay); - allowedTerminalCommandsMacOS.setDefaultValue("git, ls, cat, grep, find"); + allowedTerminalCommandsMacOS.setDefaultValue( + "git, ls, cat, grep, find, pwd, echo, head, tail, wc, which, file, stat, tree, uname, " + "date, whoami, hostname"); allowedTerminalCommandsWindows.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS); allowedTerminalCommandsWindows.setLabelText(Tr::tr("Allowed Commands (Windows)")); @@ -149,7 +153,8 @@ ToolsSettings::ToolsSettings() Tr::tr("Comma-separated list of terminal commands that AI is allowed to execute on Windows. " "Example: git, dir, type, findstr, where, cmake")); allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay); - allowedTerminalCommandsWindows.setDefaultValue("git, dir, type, findstr, where"); + allowedTerminalCommandsWindows.setDefaultValue( + "git, dir, type, findstr, where, echo, whoami, hostname, ver, tree, fc"); terminalCommandTimeout.setSettingsKey(Constants::CA_TERMINAL_COMMAND_TIMEOUT); terminalCommandTimeout.setLabelText(Tr::tr("Command Timeout (seconds)")); diff --git a/sources/external/llmqore b/sources/external/llmqore index ddbc38f..95a7b3a 160000 --- a/sources/external/llmqore +++ b/sources/external/llmqore @@ -1 +1 @@ -Subproject commit ddbc38ffbd47d553774ec1895fce53911cf3da73 +Subproject commit 95a7b3ac51b8f47bc55deffc6b3181d47c8a8f24 diff --git a/tools/CreateNewFileTool.cpp b/tools/CreateNewFileTool.cpp index c656822..f5780f4 100644 --- a/tools/CreateNewFileTool.cpp +++ b/tools/CreateNewFileTool.cpp @@ -77,10 +77,20 @@ QFuture CreateNewFileTool::executeAsync(const QJsonObject & 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. " - "Enable 'Allow file access outside project' in settings to create files outside project scope.") - .arg(absolutePath)); + 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)); } diff --git a/tools/EditFileTool.cpp b/tools/EditFileTool.cpp index 8c46bf1..26e86ec 100644 --- a/tools/EditFileTool.cpp +++ b/tools/EditFileTool.cpp @@ -143,10 +143,20 @@ QFuture EditFileTool::executeAsync(const QJsonObject &input 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. " - "Enable 'Allow file access outside project' in settings to edit files outside the project.") - .arg(filePath)); + 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)); } diff --git a/tools/ExecuteTerminalCommandTool.cpp b/tools/ExecuteTerminalCommandTool.cpp index 6154957..f2b797e 100644 --- a/tools/ExecuteTerminalCommandTool.cpp +++ b/tools/ExecuteTerminalCommandTool.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -45,18 +46,26 @@ QJsonObject ExecuteTerminalCommandTool::parametersSchema() const QJsonObject definition; definition["type"] = "object"; - const QString commandDesc = getCommandDescription(); + const QStringList allowed = getAllowedCommands(); + const QString allowedList = allowed.isEmpty() ? "none" : allowed.join(", "); QJsonObject properties; properties["command"] = QJsonObject{ {"type", "string"}, - {"description", commandDesc}}; + {"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 for the command. Arguments with spaces should be properly quoted. " - "Example: '--file \"path with spaces.txt\" --verbose'"}}; + "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"}; @@ -68,14 +77,25 @@ QFuture ExecuteTerminalCommandTool::executeAsync(const QJso { using LLMQore::ToolResult; - const QString command = input.value("command").toString().trimmed(); - const QString args = input.value("args").toString().trimmed(); + 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()));