mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-05-30 02:49:12 -04:00
feat: Add skills feature for tool and chat calling (#351)
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
Utils::BoolAspect enableQodeAssist{this};
|
||||
Utils::BoolAspect useGlobalSettings{this};
|
||||
Utils::FilePathAspect chatHistoryPath{this};
|
||||
Utils::StringAspect projectSkillDirs{this};
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
135
settings/SkillsSettings.cpp
Normal 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
|
||||
25
settings/SkillsSettings.hpp
Normal file
25
settings/SkillsSettings.hpp
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user