feat: Add skills feature for tool and chat calling (#351)

This commit is contained in:
Petr Mironychev
2026-05-19 09:46:50 +02:00
committed by GitHub
parent a3ad314cd4
commit 7483c78777
41 changed files with 1379 additions and 30 deletions

View File

@@ -9,6 +9,7 @@ add_library(QodeAssistSettings STATIC
ChatAssistantSettings.hpp ChatAssistantSettings.cpp
QuickRefactorSettings.hpp QuickRefactorSettings.cpp
ToolsSettings.hpp ToolsSettings.cpp
SkillsSettings.hpp SkillsSettings.cpp
McpSettings.hpp McpSettings.cpp
SettingsDialog.hpp SettingsDialog.cpp
ProjectSettings.hpp ProjectSettings.cpp
@@ -30,5 +31,6 @@ target_link_libraries(QodeAssistSettings
QtCreator::Core
QtCreator::Utils
QodeAssistLogger
Skills
)
target_include_directories(QodeAssistSettings PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -32,6 +32,15 @@ ProjectSettings::ProjectSettings(ProjectExplorer::Project *project)
chatHistoryPath.setDefaultValue(projectChatHistoryPath);
projectSkillDirs.setSettingsKey(Constants::SK_PROJECT_SKILL_DIRS);
projectSkillDirs.setLabelText(Tr::tr("Skill directories:"));
projectSkillDirs.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
projectSkillDirs.setToolTip(
Tr::tr("Project-relative subdirectories scanned for Agent Skills, one per line. "
"Resolved against the project root. These take priority over the global "
"skill directories when a skill name appears in both."));
projectSkillDirs.setDefaultValue(".qodeassist/skills\n.claude/skills");
Utils::Store map = Utils::storeFromVariant(
project->namedSettings(Constants::QODE_ASSIST_PROJECT_SETTINGS_ID));
fromMap(map);
@@ -39,6 +48,7 @@ ProjectSettings::ProjectSettings(ProjectExplorer::Project *project)
enableQodeAssist.addOnChanged(this, [this, project] { save(project); });
useGlobalSettings.addOnChanged(this, [this, project] { save(project); });
chatHistoryPath.addOnChanged(this, [this, project] { save(project); });
projectSkillDirs.addOnChanged(this, [this, project] { save(project); });
}
void ProjectSettings::setUseGlobalSettings(bool useGlobal)

View File

@@ -23,6 +23,7 @@ public:
Utils::BoolAspect enableQodeAssist{this};
Utils::BoolAspect useGlobalSettings{this};
Utils::FilePathAspect chatHistoryPath{this};
Utils::StringAspect projectSkillDirs{this};
};
} // namespace QodeAssist::Settings

View File

@@ -8,9 +8,15 @@
#include <projectexplorer/projectsettingswidget.h>
#include <utils/layoutbuilder.h>
#include <QLabel>
#include <QListWidget>
#include "ProjectSettings.hpp"
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include "SkillsSettings.hpp"
#include "sources/skills/SkillsLoader.hpp"
#include "sources/skills/SkillsManager.hpp"
using namespace ProjectExplorer;
@@ -41,17 +47,59 @@ static ProjectSettingsWidget *createProjectPanel(Project *project)
&ProjectSettings::setUseGlobalSettings);
widget->setUseGlobalSettings(settings->useGlobalSettings());
widget->setEnabled(!settings->useGlobalSettings());
QObject::connect(
widget, &ProjectSettingsWidget::useGlobalSettingsChanged, widget, [widget](bool useGlobal) {
widget->setEnabled(!useGlobal);
});
auto generalWidget = new QWidget;
Column{
settings->enableQodeAssist,
Space{8},
settings->chatHistoryPath,
}
.attachTo(generalWidget);
generalWidget->setEnabled(!settings->useGlobalSettings());
QObject::connect(
widget,
&ProjectSettingsWidget::useGlobalSettingsChanged,
generalWidget,
[generalWidget](bool useGlobal) { generalWidget->setEnabled(!useGlobal); });
auto skillsList = new QListWidget;
skillsList->setSelectionMode(QAbstractItemView::NoSelection);
skillsList->setMaximumHeight(160);
auto refreshSkills = [skillsList, project, settings] {
skillsList->clear();
// Project-only roots: the global page shows global skills separately.
const QStringList roots = Skills::SkillsManager::resolveRoots(
project->projectDirectory().toFSPathString(),
{},
SkillsSettings::splitLines(settings->projectSkillDirs()));
const QVector<Skills::AgentSkill> skills = Skills::SkillsLoader::scan(roots);
for (const Skills::AgentSkill &skill : skills) {
auto *item = new QListWidgetItem(
QStringLiteral("%1 — %2").arg(skill.name, skill.description), skillsList);
item->setToolTip(skill.skillDir);
}
if (skills.isEmpty())
new QListWidgetItem(Tr::tr("No skills discovered."), skillsList);
};
refreshSkills();
QObject::connect(
&settings->projectSkillDirs, &Utils::BaseAspect::changed, skillsList, refreshSkills);
Column{
generalWidget,
Space{8},
Group{
title(Tr::tr("Skills")),
Column{
settings->projectSkillDirs,
new QLabel(Tr::tr("Discovered project skills:")),
skillsList,
},
},
}
.attachTo(widget);

View File

@@ -102,12 +102,18 @@ const char CA_ENABLE_TERMINAL_COMMAND_TOOL[] = "QodeAssist.caEnableTerminalComma
const char CA_ENABLE_TODO_TOOL[] = "QodeAssist.caEnableTodoToolV2";
const char CA_ENABLE_READ_ORIGINAL_HISTORY_TOOL[]
= "QodeAssist.caEnableReadOriginalHistoryTool";
const char CA_ENABLE_SKILL_TOOL[] = "QodeAssist.caEnableSkillTool";
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";
const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows";
const char CA_TERMINAL_COMMAND_TIMEOUT[] = "QodeAssist.caTerminalCommandTimeout";
// Skills settings
const char SK_ENABLE_SKILLS[] = "QodeAssist.skEnableSkills";
const char SK_GLOBAL_SKILL_ROOTS[] = "QodeAssist.skGlobalSkillRoots";
const char SK_PROJECT_SKILL_DIRS[] = "QodeAssist.skProjectSkillDirs";
// MCP server settings
const char MCP_ENABLE_SERVER[] = "QodeAssist.mcpEnableServer";
const char MCP_SERVER_PORT[] = "QodeAssist.mcpServerPort";
@@ -124,6 +130,7 @@ const char QODE_ASSIST_QUICK_REFACTOR_SETTINGS_PAGE_ID[]
= "QodeAssist.4QuickRefactorSettingsPageId";
const char QODE_ASSIST_TOOLS_SETTINGS_PAGE_ID[] = "QodeAssist.5ToolsSettingsPageId";
const char QODE_ASSIST_MCP_SETTINGS_PAGE_ID[] = "QodeAssist.6McpSettingsPageId";
const char QODE_ASSIST_SKILLS_SETTINGS_PAGE_ID[] = "QodeAssist.8SkillsSettingsPageId";
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist";

135
settings/SkillsSettings.cpp Normal file
View File

@@ -0,0 +1,135 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "SkillsSettings.hpp"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <QDir>
#include <QLabel>
#include <QListWidget>
#include "SettingsConstants.hpp"
#include "SettingsTr.hpp"
#include "sources/skills/SkillsLoader.hpp"
namespace QodeAssist::Settings {
SkillsSettings &skillsSettings()
{
static SkillsSettings settings;
return settings;
}
QStringList SkillsSettings::splitLines(const QString &value)
{
QStringList lines;
const auto parts = value.split('\n', Qt::SkipEmptyParts);
for (const QString &part : parts) {
const QString trimmed = part.trimmed();
if (!trimmed.isEmpty())
lines << trimmed;
}
return lines;
}
QStringList SkillsSettings::splitPaths(const QString &value)
{
QStringList paths;
for (QString path : splitLines(value)) {
if (path == QLatin1String("~"))
path = QDir::homePath();
else if (path.startsWith(QLatin1String("~/")))
path = QDir::homePath() + path.mid(1);
paths << QDir::cleanPath(path);
}
return paths;
}
SkillsSettings::SkillsSettings()
{
setAutoApply(false);
setDisplayName(Tr::tr("Skills"));
enableSkills.setSettingsKey(Constants::SK_ENABLE_SKILLS);
enableSkills.setLabelText(Tr::tr("Enable skills"));
enableSkills.setToolTip(
Tr::tr("Discover Agent Skills from the configured skill directories and expose them "
"to the chat assistant. Each skill is a folder containing a SKILL.md file."));
enableSkills.setDefaultValue(true);
const QString defaultGlobalRoots
= Core::ICore::userResourcePath().toFSPathString() + "/qodeassist/skills\n"
+ QDir::homePath() + "/.claude/skills";
globalSkillRoots.setSettingsKey(Constants::SK_GLOBAL_SKILL_ROOTS);
globalSkillRoots.setLabelText(Tr::tr("Global skill directories:"));
globalSkillRoots.setDisplayStyle(Utils::StringAspect::TextEditDisplay);
globalSkillRoots.setToolTip(
Tr::tr("Absolute paths scanned for skills, one per line. Each path is a directory "
"whose subfolders contain SKILL.md files. A leading ~ expands to your home "
"directory. Lets QodeAssist pick up skills shared with other agents "
"(e.g. ~/.claude/skills)."));
globalSkillRoots.setDefaultValue(defaultGlobalRoots);
readSettings();
setLayouter([this]() {
using namespace Layouting;
auto skillsList = new QListWidget;
skillsList->setSelectionMode(QAbstractItemView::NoSelection);
skillsList->setMaximumHeight(160);
auto refreshSkills = [skillsList, this] {
skillsList->clear();
const QVector<Skills::AgentSkill> skills
= Skills::SkillsLoader::scan(splitPaths(globalSkillRoots()));
for (const Skills::AgentSkill &skill : skills) {
auto *item = new QListWidgetItem(
QStringLiteral("%1 — %2").arg(skill.name, skill.description), skillsList);
item->setToolTip(skill.skillDir);
}
if (skills.isEmpty())
new QListWidgetItem(Tr::tr("No skills discovered."), skillsList);
};
refreshSkills();
connect(&globalSkillRoots, &Utils::BaseAspect::changed, skillsList, refreshSkills);
return Column{
Group{
title(Tr::tr("Skills")),
Column{
Row{enableSkills, Stretch{1}},
},
},
Group{
title(Tr::tr("Skill Directories")),
Column{
globalSkillRoots,
new QLabel(Tr::tr("Discovered global skills:")),
skillsList,
},
},
Stretch{1}};
});
}
class SkillsSettingsPage : public Core::IOptionsPage
{
public:
SkillsSettingsPage()
{
setId(Constants::QODE_ASSIST_SKILLS_SETTINGS_PAGE_ID);
setDisplayName(Tr::tr("Skills"));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
setSettingsProvider([] { return &skillsSettings(); });
}
};
const SkillsSettingsPage skillsSettingsPage;
} // namespace QodeAssist::Settings

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <utils/aspects.h>
namespace QodeAssist::Settings {
class SkillsSettings : public Utils::AspectContainer
{
public:
SkillsSettings();
Utils::BoolAspect enableSkills{this};
Utils::StringAspect globalSkillRoots{this};
static QStringList splitLines(const QString &value);
static QStringList splitPaths(const QString &value);
};
SkillsSettings &skillsSettings();
} // namespace QodeAssist::Settings

View File

@@ -119,6 +119,14 @@ ToolsSettings::ToolsSettings()
"summary currently in context. Has no effect if the chat was never compressed."));
enableReadOriginalHistoryTool.setDefaultValue(true);
enableSkillTool.setSettingsKey(Constants::CA_ENABLE_SKILL_TOOL);
enableSkillTool.setLabelText(Tr::tr("Load Skill"));
enableSkillTool.setToolTip(
Tr::tr("Lets the AI load the full instructions of a skill on demand. The Available "
"Skills catalog in the system prompt lists each skill; this tool pulls a "
"skill's complete instructions into context when needed."));
enableSkillTool.setDefaultValue(true);
allowedTerminalCommandsLinux.setSettingsKey(Constants::CA_ALLOWED_TERMINAL_COMMANDS_LINUX);
allowedTerminalCommandsLinux.setLabelText(Tr::tr("Allowed Commands (Linux)"));
allowedTerminalCommandsLinux.setToolTip(
@@ -186,7 +194,8 @@ ToolsSettings::ToolsSettings()
enableGetIssuesListTool,
enableTerminalCommandTool,
enableTodoTool,
enableReadOriginalHistoryTool}},
enableReadOriginalHistoryTool,
enableSkillTool}},
Space{8},
Group{
title(Tr::tr("Tool Settings")),
@@ -237,6 +246,7 @@ void ToolsSettings::resetSettingsToDefaults()
resetAspect(enableTerminalCommandTool);
resetAspect(enableTodoTool);
resetAspect(enableReadOriginalHistoryTool);
resetAspect(enableSkillTool);
resetAspect(allowedTerminalCommandsLinux);
resetAspect(allowedTerminalCommandsMacOS);
resetAspect(allowedTerminalCommandsWindows);

View File

@@ -31,6 +31,7 @@ public:
Utils::BoolAspect enableTerminalCommandTool{this};
Utils::BoolAspect enableTodoTool{this};
Utils::BoolAspect enableReadOriginalHistoryTool{this};
Utils::BoolAspect enableSkillTool{this};
Utils::StringAspect allowedTerminalCommandsLinux{this};
Utils::StringAspect allowedTerminalCommandsMacOS{this};