feat: Add agent roles (#287)

* feat: Add agent roles
* doc: Add agent roles to docs
This commit is contained in:
Petr Mironychev
2025-12-04 19:41:30 +01:00
committed by GitHub
parent 498eb4d932
commit fc33bb60d0
22 changed files with 1713 additions and 293 deletions

View File

@ -21,7 +21,7 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/AttachedFilesPlace.qml qml/controls/AttachedFilesPlace.qml
qml/controls/BottomBar.qml qml/controls/BottomBar.qml
qml/controls/FileEditsActionBar.qml qml/controls/FileEditsActionBar.qml
qml/controls/RulesViewer.qml qml/controls/ContextViewer.qml
qml/controls/Toast.qml qml/controls/Toast.qml
qml/controls/TopBar.qml qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml qml/controls/SplitDropZone.qml
@ -43,6 +43,7 @@ qt_add_qml_module(QodeAssistChatView
icons/chat-icon.svg icons/chat-icon.svg
icons/chat-pause-icon.svg icons/chat-pause-icon.svg
icons/rules-icon.svg icons/rules-icon.svg
icons/context-icon.svg
icons/open-in-editor.svg icons/open-in-editor.svg
icons/apply-changes-button.svg icons/apply-changes-button.svg
icons/undo-changes-button.svg icons/undo-changes-button.svg

View File

@ -34,6 +34,7 @@
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include "AgentRole.hpp"
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
@ -117,6 +118,11 @@ ChatRootView::ChatRootView(QQuickItem *parent)
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
this, this,
&ChatRootView::updateInputTokensCount); &ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::baseSystemPromptChanged);
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@ -209,6 +215,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
updateInputTokensCount(); updateInputTokensCount();
refreshRules(); refreshRules();
loadAvailableConfigurations(); loadAvailableConfigurations();
loadAvailableAgentRoles();
connect( connect(
ProjectExplorer::ProjectManager::instance(), ProjectExplorer::ProjectManager::instance(),
@ -1366,4 +1373,101 @@ QString ChatRootView::currentConfiguration() const
return m_currentConfiguration; return m_currentConfiguration;
} }
void ChatRootView::loadAvailableAgentRoles()
{
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
m_availableAgentRoles.clear();
m_availableAgentRoles.append(Settings::AgentRolesManager::getNoRole().name);
for (const auto &role : roles)
m_availableAgentRoles.append(role.name);
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
m_currentAgentRole = Settings::AgentRolesManager::getNoRole().name;
if (!lastRoleId.isEmpty()) {
for (const auto &role : roles) {
if (role.id == lastRoleId) {
m_currentAgentRole = role.name;
break;
}
}
}
emit availableAgentRolesChanged();
emit currentAgentRoleChanged();
}
void ChatRootView::applyAgentRole(const QString &roleName)
{
auto &settings = Settings::chatAssistantSettings();
if (roleName == Settings::AgentRolesManager::getNoRole().name) {
settings.lastUsedRoleId.setValue("");
settings.writeSettings();
m_currentAgentRole = roleName;
emit currentAgentRoleChanged();
return;
}
const QList<Settings::AgentRole> roles = Settings::AgentRolesManager::loadAllRoles();
for (const auto &role : roles) {
if (role.name == roleName) {
settings.lastUsedRoleId.setValue(role.id);
settings.writeSettings();
m_currentAgentRole = role.name;
emit currentAgentRoleChanged();
break;
}
}
}
QStringList ChatRootView::availableAgentRoles() const
{
return m_availableAgentRoles;
}
QString ChatRootView::currentAgentRole() const
{
return m_currentAgentRole;
}
QString ChatRootView::baseSystemPrompt() const
{
return Settings::chatAssistantSettings().systemPrompt();
}
QString ChatRootView::currentAgentRoleDescription() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return Settings::AgentRolesManager::getNoRole().description;
return role.description;
}
QString ChatRootView::currentAgentRoleSystemPrompt() const
{
const QString lastRoleId = Settings::chatAssistantSettings().lastUsedRoleId();
if (lastRoleId.isEmpty())
return QString();
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (role.id.isEmpty())
return QString();
return role.systemPrompt;
}
void ChatRootView::openAgentRolesSettings()
{
Core::ICore::showOptionsDialog(Utils::Id("QodeAssist.AgentRoles"));
}
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@ -59,6 +59,11 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL) Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL) Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
QML_ELEMENT QML_ELEMENT
@ -146,6 +151,15 @@ public:
QStringList availableConfigurations() const; QStringList availableConfigurations() const;
QString currentConfiguration() const; QString currentConfiguration() const;
Q_INVOKABLE void loadAvailableAgentRoles();
Q_INVOKABLE void applyAgentRole(const QString &roleId);
Q_INVOKABLE void openAgentRolesSettings();
QStringList availableAgentRoles() const;
QString currentAgentRole() const;
QString baseSystemPrompt() const;
QString currentAgentRoleDescription() const;
QString currentAgentRoleSystemPrompt() const;
int currentMessageTotalEdits() const; int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const; int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const; int currentMessagePendingEdits() const;
@ -191,6 +205,9 @@ signals:
void isThinkingSupportChanged(); void isThinkingSupportChanged();
void availableConfigurationsChanged(); void availableConfigurationsChanged();
void currentConfigurationChanged(); void currentConfigurationChanged();
void availableAgentRolesChanged();
void currentAgentRoleChanged();
void baseSystemPromptChanged();
private: private:
void updateFileEditStatus(const QString &editId, const QString &status); void updateFileEditStatus(const QString &editId, const QString &status);
@ -224,6 +241,9 @@ private:
QStringList m_availableConfigurations; QStringList m_availableConfigurations;
QString m_currentConfiguration; QString m_currentConfiguration;
QStringList m_availableAgentRoles;
QString m_currentAgentRole;
}; };
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@ -160,6 +160,13 @@ void ClientInterface::sendMessage(
if (chatAssistantSettings.useSystemPrompt()) { if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt(); QString systemPrompt = chatAssistantSettings.systemPrompt();
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
if (!lastRoleId.isEmpty()) {
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (!role.id.isEmpty())
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
}
auto project = LLMCore::RulesLoader::getActiveProject(); auto project = LLMCore::RulesLoader::getActiveProject();
if (project) { if (project) {

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@ -26,6 +26,7 @@ import UIControls
import Qt.labs.platform as Platform import Qt.labs.platform as Platform
import "./chatparts" import "./chatparts"
import "./controls"
ChatRootView { ChatRootView {
id: root id: root
@ -97,8 +98,7 @@ ChatRootView {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved") text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
} }
openChatHistory.onClicked: root.openChatHistoryFolder() openChatHistory.onClicked: root.openChatHistoryFolder()
rulesButton.onClicked: rulesViewer.open() contextButton.onClicked: contextViewer.open()
activeRulesCount: root.activeRulesCount
pinButton { pinButton {
visible: typeof _chatview !== 'undefined' visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
@ -131,6 +131,18 @@ ChatRootView {
root.loadAvailableConfigurations() root.loadAvailableConfigurations()
} }
} }
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
}
}
} }
ListView { ListView {
@ -479,19 +491,28 @@ ChatRootView {
toastTextColor: "#FFFFFF" toastTextColor: "#FFFFFF"
} }
RulesViewer { ContextViewer {
id: rulesViewer id: contextViewer
width: parent.width * 0.8 width: Math.min(parent.width * 0.85, 800)
height: parent.height * 0.8 height: Math.min(parent.height * 0.85, 700)
x: (parent.width - width) / 2 x: (parent.width - width) / 2
y: (parent.height - height) / 2 y: (parent.height - height) / 2
baseSystemPrompt: root.baseSystemPrompt
currentAgentRole: root.currentAgentRole
currentAgentRoleDescription: root.currentAgentRoleDescription
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
activeRules: root.activeRules activeRules: root.activeRules
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex) activeRulesCount: root.activeRulesCount
onRefreshRules: root.refreshRules() onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder() onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
} }
Connections { Connections {

View File

@ -0,0 +1,558 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property string baseSystemPrompt
property string currentAgentRole
property string currentAgentRoleDescription
property string currentAgentRoleSystemPrompt
property var activeRules
property int activeRulesCount
property string selectedRuleContent
signal openSettings()
signal openAgentRolesSettings()
signal openRulesFolder()
signal refreshRules()
signal ruleSelected(int index)
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Chat Context")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Flickable {
id: mainFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: sectionsColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: sectionsColumn
width: mainFlickable.width
spacing: 8
CollapsibleSection {
id: systemPromptSection
Layout.fillWidth: true
title: qsTr("Base System Prompt")
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 5
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
Flickable {
id: systemPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: systemPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: systemPromptText
width: systemPromptFlickable.width
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.baseSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
}
QoAButton {
text: qsTr("Edit in Settings")
onClicked: {
root.openSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: agentRoleSection
Layout.fillWidth: true
title: qsTr("Agent Role")
badge: root.currentAgentRole
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
Text {
text: root.currentAgentRoleDescription
font.pixelSize: 11
font.italic: true
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleDescription.length > 0
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
visible: root.currentAgentRoleSystemPrompt.length > 0
Flickable {
id: agentPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: agentPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: agentPromptText
width: agentPromptFlickable.width
text: root.currentAgentRoleSystemPrompt
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
Text {
text: qsTr("No role selected. Using base system prompt only.")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleSystemPrompt.length === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.currentAgentRoleSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
}
QoAButton {
text: qsTr("Manage Roles")
onClicked: {
root.openAgentRolesSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: projectRulesSection
Layout.fillWidth: true
title: qsTr("Project Rules")
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
SplitView {
Layout.fillWidth: true
Layout.preferredHeight: 220
orientation: Qt.Horizontal
visible: root.activeRulesCount > 0
Rectangle {
SplitView.minimumWidth: 120
SplitView.preferredWidth: 180
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules (%1)").arg(rulesList.count)
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
boundsBehavior: Flickable.StopAtBounds
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
height: ruleItemContent.implicitHeight + 8
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted)
return palette.highlight
if (parent.hovered)
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
id: ruleItemContent
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 10
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: modelData.category
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
root.ruleSelected(index)
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
Rectangle {
SplitView.fillWidth: true
SplitView.minimumWidth: 200
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: root.selectedRuleContent.length > 0
onClicked: utils.copyToClipboard(root.selectedRuleContent)
}
}
Flickable {
id: ruleContentFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: ruleContentArea.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: ruleContentArea
width: ruleContentFlickable.width
text: root.selectedRuleContent
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
}
Text {
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
visible: root.activeRulesCount === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Open Rules Folder")
onClicked: root.openRulesFolder()
}
}
}
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Text {
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
component CollapsibleSection: ColumnLayout {
id: sectionRoot
property string title
property string badge
property color badgeColor: palette.mid
property Component sectionContent: null
property bool expanded: false
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
border.color: palette.mid
border.width: 1
radius: 2
MouseArea {
id: sectionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: sectionRoot.expanded = !sectionRoot.expanded
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
text: sectionRoot.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.text
}
Text {
text: sectionRoot.title
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
Rectangle {
implicitWidth: badgeText.implicitWidth + 12
implicitHeight: 18
color: sectionRoot.badgeColor
radius: 3
Text {
id: badgeText
anchors.centerIn: parent
text: sectionRoot.badge
font.pixelSize: 10
color: "#FFFFFF"
}
}
}
}
Loader {
id: contentLoader
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 8
Layout.bottomMargin: 4
sourceComponent: sectionRoot.sectionContent
visible: sectionRoot.expanded
active: sectionRoot.expanded
}
}
onOpened: {
if (root.activeRulesCount > 0) {
root.ruleSelected(0)
}
}
}

View File

@ -1,251 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property var activeRules
property alias rulesCurrentIndex: rulesList.currentIndex
property alias ruleContentAreaText: ruleContentArea.text
signal refreshRules()
signal openRulesFolder()
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Active Project Rules")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Open Folder")
onClicked: root.openRulesFolder()
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
SplitView {
Layout.fillWidth: true
Layout.fillHeight: true
orientation: Qt.Horizontal
Rectangle {
SplitView.minimumWidth: 200
SplitView.preferredWidth: parent.width * 0.3
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules Files (%1)").arg(rulesList.count)
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted) {
return palette.highlight
} else if (parent.hovered) {
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
}
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 11
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: qsTr("Category: %1").arg(modelData.category)
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
}
}
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
}
Text {
visible: rulesList.count === 0
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
font.pixelSize: 10
color: palette.mid
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignCenter
}
}
}
Rectangle {
SplitView.fillWidth: true
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: ruleContentArea.text.length > 0
onClicked: utils.copyToClipboard(ruleContentArea.text)
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
TextEdit {
id: ruleContentArea
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
}
}
}
}
Text {
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}

View File

@ -33,12 +33,12 @@ Rectangle {
property alias recentPath: recentPathId property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId property alias pinButton: pinButtonId
property alias rulesButton: rulesButtonId property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId property alias settingsButton: settingsButtonId
property alias activeRulesCount: activeRulesCountId.text
property alias configSelector: configSelectorId property alias configSelector: configSelectorId
property alias roleSelector: roleSelector
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@ -90,6 +90,19 @@ Rectangle {
ToolTip.text: qsTr("Switch AI configuration") ToolTip.text: qsTr("Switch AI configuration")
} }
QoAComboBox {
id: roleSelector
implicitHeight: 25
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch agent role (different system prompts)")
}
QoAButton { QoAButton {
id: toolsButtonId id: toolsButtonId
@ -244,35 +257,18 @@ Rectangle {
} }
QoAButton { QoAButton {
id: rulesButtonId id: contextButtonId
icon { icon {
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg" source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15 height: 15
width: 15 width: 15
} }
text: " "
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.delay: 250 ToolTip.delay: 250
ToolTip.text: root.activeRulesCount > 0 ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
: qsTr("View active project rules (no rules found)")
Text {
id: activeRulesCountId
anchors {
bottom: parent.bottom
bottomMargin: 2
right: parent.right
rightMargin: 4
}
color: palette.text
font.pixelSize: 10
font.bold: true
}
} }
Badge { Badge {

View File

@ -158,6 +158,7 @@ For optimal coding assistance, we recommend using these top-tier models:
### Additional Configuration ### Additional Configuration
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project - **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore` - **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
@ -194,6 +195,7 @@ Configure in: `Tools → Options → QodeAssist → Code Completion → General
- Multiple chat panels: side panel, bottom panel, and popup window - Multiple chat panels: side panel, bottom panel, and popup window
- Chat history with auto-save and restore - Chat history with auto-save and restore
- Token usage monitoring - Token usage monitoring
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
- **[File Context](docs/file-context.md)** - Attach or link files for better context - **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional) - Automatic syncing with open editor files (optional)
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks - Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
@ -278,22 +280,24 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
│ CHAT ASSISTANT │ │ CHAT ASSISTANT │
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Chat Assistant Settings) │ │ 1. System Prompt (from Chat Assistant Settings) │
│ 2. Project Rules: │ 2. Agent Role (optional, from role selector):
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
│ 3. Project Rules: │
│ ├─ .qodeassist/rules/common/*.md │ │ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/chat/*.md │ │ └─ .qodeassist/rules/chat/*.md │
3. File Context (optional): │ 4. File Context (optional): │
│ ├─ Attached files (manual) │ │ ├─ Attached files (manual) │
│ ├─ Linked files (persistent) │ │ ├─ Linked files (persistent) │
│ └─ Open editor files (if auto-sync enabled) │ │ └─ Open editor files (if auto-sync enabled) │
4. Tool Definitions (if enabled): │ 5. Tool Definitions (if enabled): │
│ ├─ ReadProjectFileByName │ │ ├─ ReadProjectFileByName │
│ ├─ ListProjectFiles │ │ ├─ ListProjectFiles │
│ ├─ SearchInProject │ │ ├─ SearchInProject │
│ └─ GetIssuesList │ │ └─ GetIssuesList │
5. Conversation History │ 6. Conversation History │
6. User Message │ 7. User Message │
│ │ │ │
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
│ [History: Previous messages] │ │ [History: Previous messages] │
│ [User: FileContext + UserMessage] │ │ [User: FileContext + UserMessage] │
└─────────────────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────────────────┘
@ -339,6 +343,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure - **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
- **System Prompts** are configured independently for each feature in Settings - **System Prompts** are configured independently for each feature in Settings
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
- **FIM vs Non-FIM models** for code completion use different System Prompts: - **FIM vs Non-FIM models** for code completion use different System Prompts:
- FIM models: Direct completion prompt - FIM models: Direct completion prompt
- Non-FIM models: Prompt includes response formatting instructions - Non-FIM models: Prompt includes response formatting instructions
@ -346,7 +351,7 @@ QodeAssist uses a flexible prompt composition system that adapts to different co
- **Custom Instructions** provide reusable templates that can be augmented with specific details - **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled - **Tool Calling** is available for Chat and Quick Refactor when enabled
See [Project Rules Documentation](docs/project-rules.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details. See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
## QtCreator Version Compatibility ## QtCreator Version Compatibility

View File

@ -27,6 +27,27 @@ Basic.ComboBox {
implicitWidth: Math.min(contentItem.implicitWidth + 8, 300) implicitWidth: Math.min(contentItem.implicitWidth + 8, 300)
implicitHeight: 30 implicitHeight: 30
property real popupContentWidth: 100
TextMetrics {
id: textMetrics
font.pixelSize: 12
}
function updatePopupWidth() {
var maxWidth = 100;
if (model) {
for (var i = 0; i < model.length; i++) {
textMetrics.text = model[i];
maxWidth = Math.max(maxWidth, textMetrics.width + 40);
}
}
popupContentWidth = Math.min(maxWidth, 350);
}
onModelChanged: updatePopupWidth()
Component.onCompleted: updatePopupWidth()
indicator: Image { indicator: Image {
id: dropdownIcon id: dropdownIcon
@ -94,7 +115,7 @@ Basic.ComboBox {
popup: Popup { popup: Popup {
y: control.height + 2 y: control.height + 2
width: control.width width: Math.max(control.width, control.popupContentWidth)
implicitHeight: Math.min(contentItem.implicitHeight, 300) implicitHeight: Math.min(contentItem.implicitHeight, 300)
padding: 4 padding: 4
@ -128,7 +149,7 @@ Basic.ComboBox {
} }
delegate: ItemDelegate { delegate: ItemDelegate {
width: control.width - 8 width: control.popup.width - 8
height: 32 height: 32
contentItem: Text { contentItem: Text {

171
docs/agent-roles.md Normal file
View File

@ -0,0 +1,171 @@
# Agent Roles
Agent Roles allow you to define different AI personas with specialized system prompts for various tasks. Switch between roles instantly in the chat interface to adapt the AI's behavior to your current needs.
## Overview
Agent Roles are reusable system prompt configurations that modify how the AI assistant responds. Instead of manually changing system prompts, you can create roles like "Developer", "Code Reviewer", or "Documentation Writer" and switch between them with a single click.
**Key Features:**
- **Quick Switching**: Change roles from the chat toolbar dropdown
- **Custom Prompts**: Each role has its own specialized system prompt
- **Built-in Roles**: Pre-configured Developer and Code Reviewer roles
- **Persistent**: Roles are saved locally and loaded on startup
- **Extensible**: Create unlimited custom roles for different tasks
## Default Roles
QodeAssist comes with two built-in roles:
### Developer
General coding assistance focused on writing clean, maintainable code following industry standards. Best for implementation tasks, debugging, and code explanations.
### Code Reviewer
Expert code review persona that identifies bugs, performance issues, and adherence to best practices. Provides constructive feedback with specific suggestions for improvement.
## Using Agent Roles
### Switching Roles in Chat
1. Open the Chat Assistant (side panel, bottom panel, or popup window)
2. Locate the **Role selector** dropdown in the top toolbar (next to the configuration selector)
3. Select a role from the dropdown
4. The AI will now use the selected role's system prompt
**Note**: Selecting "No Role" uses only the base system prompt without role specialization.
### Viewing Active Role
Click the **Context** button (📋) in the chat toolbar to view:
- Base system prompt
- Current agent role and its system prompt
- Active project rules
## Managing Agent Roles
### Opening the Role Manager
Navigate to: `Qt Creator → Preferences → QodeAssist → Chat Assistant`
Scroll down to the **Agent Roles** section where you can manage all your roles.
### Creating a New Role
1. Click **Add...** button
2. Fill in the role details:
- **Name**: Display name shown in the dropdown (e.g., "Documentation Writer")
- **ID**: Unique identifier for the role file (e.g., "doc_writer")
- **Description**: Brief explanation of the role's purpose
- **System Prompt**: The specialized instructions for this role
3. Click **OK** to save
### Editing a Role
1. Select a role from the list
2. Click **Edit...** or double-click the role
3. Modify the fields as needed
4. Click **OK** to save changes
**Note**: Built-in roles cannot be edited directly. Duplicate them to create a modifiable copy.
### Duplicating a Role
1. Select a role to duplicate
2. Click **Duplicate...**
3. Modify the copy as needed
4. Click **OK** to save as a new role
### Deleting a Role
1. Select a custom role (built-in roles cannot be deleted)
2. Click **Delete**
3. Confirm deletion
## Creating Effective Roles
### System Prompt Tips
- **Be specific**: Clearly define the role's expertise and focus areas
- **Set expectations**: Describe the desired response format and style
- **Include guidelines**: Add specific rules or constraints for responses
- **Use structured prompts**: Break down complex roles into bullet points
## Storage Location
Agent roles are stored as JSON files in:
```
~/.config/QtProject/qtcreator/qodeassist/agent_roles/
```
**On different platforms:**
- **Linux**: `~/.config/QtProject/qtcreator/qodeassist/agent_roles/`
- **macOS**: `~/Library/Application Support/QtProject/Qt Creator/qodeassist/agent_roles/`
- **Windows**: `%APPDATA%\QtProject\qtcreator\qodeassist\agent_roles\`
### File Format
Each role is stored as a JSON file named `{id}.json`:
```json
{
"id": "doc_writer",
"name": "Documentation Writer",
"description": "Technical documentation and code comments",
"systemPrompt": "You are a technical documentation specialist...",
"isBuiltin": false
}
```
### Manual Editing
You can:
- Edit JSON files directly in any text editor
- Copy role files between machines
- Share roles with team members
- Version control your roles
- Click **Open Roles Folder...** to quickly access the directory
## How Roles Work
When a role is selected, the final system prompt is composed as:
```
┌─────────────────────────────────────────────────┐
│ Final System Prompt = Base Prompt + Role Prompt │
├─────────────────────────────────────────────────┤
│ 1. Base System Prompt (from Chat Settings) │
│ 2. Agent Role System Prompt │
│ 3. Project Rules (common/ + chat/) │
│ 4. Linked Files Context │
└─────────────────────────────────────────────────┘
```
This allows roles to augment rather than replace your base configuration.
## Best Practices
1. **Keep roles focused**: Each role should have a clear, specific purpose
2. **Use descriptive names**: Make it easy to identify roles at a glance
3. **Test your prompts**: Verify roles produce the expected behavior
4. **Iterate and improve**: Refine prompts based on AI responses
5. **Share with team**: Export and share useful roles with colleagues
## Troubleshooting
### Role Not Appearing in Dropdown
- Restart Qt Creator after adding roles manually
- Check JSON file format validity
- Verify file is in the correct directory
### Role Behavior Not as Expected
- Review the system prompt for clarity
- Check if base system prompt conflicts with role prompt
- Try a more specific or detailed prompt
## Related Documentation
- [Project Rules](project-rules.md) - Project-specific AI behavior customization
- [Chat Assistant Features](../README.md#chat-assistant) - Overview of chat functionality
- [File Context](file-context.md) - Attaching files to chat context

159
settings/AgentRole.cpp Normal file
View File

@ -0,0 +1,159 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "AgentRole.hpp"
#include <coreplugin/icore.h>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Settings {
QString AgentRolesManager::getConfigurationDirectory()
{
QString path = QString("%1/qodeassist/agent_roles")
.arg(Core::ICore::userResourcePath().toFSPathString());
QDir().mkpath(path);
return path;
}
QList<AgentRole> AgentRolesManager::loadAllRoles()
{
QList<AgentRole> roles;
QString configDir = getConfigurationDirectory();
QDir dir(configDir);
ensureDefaultRoles();
const QStringList jsonFiles = dir.entryList({"*.json"}, QDir::Files);
for (const QString &fileName : jsonFiles) {
AgentRole role = loadRoleFromFile(dir.absoluteFilePath(fileName));
if (!role.id.isEmpty()) {
roles.append(role);
}
}
return roles;
}
AgentRole AgentRolesManager::loadRole(const QString &roleId)
{
if (roleId.isEmpty())
return {};
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
if (!QFile::exists(filePath))
return {};
return loadRoleFromFile(filePath);
}
AgentRole AgentRolesManager::loadRoleFromFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly))
return {};
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
if (doc.isNull() || !doc.isObject())
return {};
return AgentRole::fromJson(doc.object());
}
bool AgentRolesManager::saveRole(const AgentRole &role)
{
if (role.id.isEmpty())
return false;
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(role.id + ".json");
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly))
return false;
QJsonDocument doc(role.toJson());
file.write(doc.toJson(QJsonDocument::Indented));
return true;
}
bool AgentRolesManager::deleteRole(const QString &roleId)
{
if (roleId.isEmpty())
return false;
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
return QFile::remove(filePath);
}
bool AgentRolesManager::roleExists(const QString &roleId)
{
if (roleId.isEmpty())
return false;
QString filePath = QDir(getConfigurationDirectory()).absoluteFilePath(roleId + ".json");
return QFile::exists(filePath);
}
void AgentRolesManager::ensureDefaultRoles()
{
QDir dir(getConfigurationDirectory());
if (!dir.exists("developer.json"))
saveRole(getDefaultDeveloperRole());
if (!dir.exists("reviewer.json"))
saveRole(getDefaultReviewerRole());
}
AgentRole AgentRolesManager::getDefaultDeveloperRole()
{
return AgentRole{
"developer",
"Developer",
"General coding assistance and implementation",
"You are an advanced AI assistant specializing in C++, Qt, and QML development. "
"Your role is to provide helpful, accurate, and detailed responses to questions "
"about coding, debugging, and best practices in these technologies. "
"Focus on writing clean, maintainable code following industry standards.",
false};
}
AgentRole AgentRolesManager::getDefaultReviewerRole()
{
return AgentRole{
"reviewer",
"Code Reviewer",
"Code review, quality assurance, and best practices",
"You are an expert code reviewer specializing in C++, Qt, and QML. "
"Your role is to:\n"
"- Identify potential bugs, memory leaks, and performance issues\n"
"- Check adherence to coding standards and best practices\n"
"- Suggest improvements for readability and maintainability\n"
"- Verify proper error handling and edge cases\n"
"- Ensure thread safety and proper Qt object lifetime management\n"
"Provide constructive, specific feedback with examples.",
false};
}
} // namespace QodeAssist::Settings

81
settings/AgentRole.hpp Normal file
View File

@ -0,0 +1,81 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QList>
#include <QString>
namespace QodeAssist::Settings {
struct AgentRole
{
QString id;
QString name;
QString description;
QString systemPrompt;
bool isBuiltin = false;
QJsonObject toJson() const
{
return QJsonObject{
{"id", id},
{"name", name},
{"description", description},
{"systemPrompt", systemPrompt},
{"isBuiltin", isBuiltin}};
}
static AgentRole fromJson(const QJsonObject &json)
{
return AgentRole{
json["id"].toString(),
json["name"].toString(),
json["description"].toString(),
json["systemPrompt"].toString(),
json["isBuiltin"].toBool(false)};
}
bool operator==(const AgentRole &other) const { return id == other.id; }
};
class AgentRolesManager
{
public:
static QString getConfigurationDirectory();
static QList<AgentRole> loadAllRoles();
static AgentRole loadRole(const QString &roleId);
static AgentRole loadRoleFromFile(const QString &filePath);
static bool saveRole(const AgentRole &role);
static bool deleteRole(const QString &roleId);
static bool roleExists(const QString &roleId);
static void ensureDefaultRoles();
static AgentRole getNoRole()
{
return AgentRole{"", "No Role", "Use base system prompt without role specialization", "", false};
}
private:
static AgentRole getDefaultDeveloperRole();
static AgentRole getDefaultReviewerRole();
};
} // namespace QodeAssist::Settings

View File

@ -0,0 +1,122 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "AgentRoleDialog.hpp"
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTextEdit>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
AgentRoleDialog::AgentRoleDialog(QWidget *parent)
: QDialog(parent)
, m_editMode(false)
{
setWindowTitle(tr("Add Agent Role"));
setupUI();
}
AgentRoleDialog::AgentRoleDialog(const AgentRole &role, bool editMode, QWidget *parent)
: QDialog(parent)
, m_editMode(editMode)
{
setWindowTitle(editMode ? tr("Edit Agent Role") : tr("Duplicate Agent Role"));
setupUI();
setRole(role);
}
void AgentRoleDialog::setupUI()
{
auto *mainLayout = new QVBoxLayout(this);
auto *formLayout = new QFormLayout();
m_nameEdit = new QLineEdit(this);
m_nameEdit->setPlaceholderText(tr("e.g., Developer, Code Reviewer"));
formLayout->addRow(tr("Name:"), m_nameEdit);
m_idEdit = new QLineEdit(this);
m_idEdit->setPlaceholderText(tr("e.g., developer, code_reviewer"));
formLayout->addRow(tr("ID:"), m_idEdit);
m_descriptionEdit = new QTextEdit(this);
m_descriptionEdit->setPlaceholderText(tr("Brief description of this role..."));
m_descriptionEdit->setMaximumHeight(80);
formLayout->addRow(tr("Description:"), m_descriptionEdit);
mainLayout->addLayout(formLayout);
auto *promptLabel = new QLabel(tr("System Prompt:"), this);
mainLayout->addWidget(promptLabel);
m_systemPromptEdit = new QTextEdit(this);
m_systemPromptEdit->setPlaceholderText(
tr("You are an expert in...\n\nYour role is to:\n- Task 1\n- Task 2\n- Task 3"));
mainLayout->addWidget(m_systemPromptEdit);
m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
mainLayout->addWidget(m_buttonBox);
connect(m_buttonBox, &QDialogButtonBox::accepted, this, &AgentRoleDialog::accept);
connect(m_buttonBox, &QDialogButtonBox::rejected, this, &AgentRoleDialog::reject);
connect(m_nameEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
connect(m_idEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
connect(m_systemPromptEdit, &QTextEdit::textChanged, this, &AgentRoleDialog::validateInput);
if (m_editMode) {
m_idEdit->setEnabled(false);
m_idEdit->setToolTip(tr("ID cannot be changed for existing roles"));
}
setMinimumSize(600, 500);
validateInput();
}
void AgentRoleDialog::validateInput()
{
bool valid = !m_nameEdit->text().trimmed().isEmpty()
&& !m_idEdit->text().trimmed().isEmpty()
&& !m_systemPromptEdit->toPlainText().trimmed().isEmpty();
m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid);
}
AgentRole AgentRoleDialog::getRole() const
{
return AgentRole{
m_idEdit->text().trimmed(),
m_nameEdit->text().trimmed(),
m_descriptionEdit->toPlainText().trimmed(),
m_systemPromptEdit->toPlainText().trimmed(),
false};
}
void AgentRoleDialog::setRole(const AgentRole &role)
{
m_idEdit->setText(role.id);
m_nameEdit->setText(role.name);
m_descriptionEdit->setPlainText(role.description);
m_systemPromptEdit->setPlainText(role.systemPrompt);
}
} // namespace QodeAssist::Settings

View File

@ -0,0 +1,55 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDialog>
#include "AgentRole.hpp"
class QLineEdit;
class QTextEdit;
class QDialogButtonBox;
namespace QodeAssist::Settings {
class AgentRoleDialog : public QDialog
{
Q_OBJECT
public:
explicit AgentRoleDialog(QWidget *parent = nullptr);
explicit AgentRoleDialog(const AgentRole &role, bool editMode = true, QWidget *parent = nullptr);
AgentRole getRole() const;
void setRole(const AgentRole &role);
private:
void setupUI();
void validateInput();
QLineEdit *m_nameEdit = nullptr;
QLineEdit *m_idEdit = nullptr;
QTextEdit *m_descriptionEdit = nullptr;
QTextEdit *m_systemPromptEdit = nullptr;
QDialogButtonBox *m_buttonBox = nullptr;
bool m_editMode = false;
};
} // namespace QodeAssist::Settings

View File

@ -0,0 +1,264 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "AgentRolesWidget.hpp"
#include "AgentRole.hpp"
#include "AgentRoleDialog.hpp"
#include "SettingsTr.hpp"
#include <QDesktopServices>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMessageBox>
#include <QPushButton>
#include <QUrl>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
AgentRolesWidget::AgentRolesWidget(QWidget *parent)
: QWidget(parent)
{
setupUI();
loadRoles();
}
void AgentRolesWidget::setupUI()
{
auto *mainLayout = new QVBoxLayout(this);
auto *headerLayout = new QHBoxLayout();
auto *infoLabel = new QLabel(
Tr::tr("Agent roles define different system prompts for specific tasks."), this);
infoLabel->setWordWrap(true);
headerLayout->addWidget(infoLabel, 1);
auto *openFolderButton = new QPushButton(Tr::tr("Open Roles Folder..."), this);
connect(openFolderButton, &QPushButton::clicked, this, &AgentRolesWidget::onOpenRolesFolder);
headerLayout->addWidget(openFolderButton);
mainLayout->addLayout(headerLayout);
auto *contentLayout = new QHBoxLayout();
m_rolesList = new QListWidget(this);
m_rolesList->setSelectionMode(QAbstractItemView::SingleSelection);
connect(m_rolesList, &QListWidget::itemSelectionChanged, this, &AgentRolesWidget::updateButtons);
connect(m_rolesList, &QListWidget::itemDoubleClicked, this, &AgentRolesWidget::onEditRole);
contentLayout->addWidget(m_rolesList, 1);
auto *buttonsLayout = new QVBoxLayout();
m_addButton = new QPushButton(Tr::tr("Add..."), this);
connect(m_addButton, &QPushButton::clicked, this, &AgentRolesWidget::onAddRole);
buttonsLayout->addWidget(m_addButton);
m_editButton = new QPushButton(Tr::tr("Edit..."), this);
connect(m_editButton, &QPushButton::clicked, this, &AgentRolesWidget::onEditRole);
buttonsLayout->addWidget(m_editButton);
m_duplicateButton = new QPushButton(Tr::tr("Duplicate..."), this);
connect(m_duplicateButton, &QPushButton::clicked, this, &AgentRolesWidget::onDuplicateRole);
buttonsLayout->addWidget(m_duplicateButton);
m_deleteButton = new QPushButton(Tr::tr("Delete"), this);
connect(m_deleteButton, &QPushButton::clicked, this, &AgentRolesWidget::onDeleteRole);
buttonsLayout->addWidget(m_deleteButton);
buttonsLayout->addStretch();
contentLayout->addLayout(buttonsLayout);
mainLayout->addLayout(contentLayout);
updateButtons();
}
void AgentRolesWidget::loadRoles()
{
m_rolesList->clear();
const QList<AgentRole> roles = AgentRolesManager::loadAllRoles();
for (const AgentRole &role : roles) {
auto *item = new QListWidgetItem(role.name, m_rolesList);
item->setData(Qt::UserRole, role.id);
QString tooltip = role.description;
if (role.isBuiltin) {
tooltip += "\n\n" + Tr::tr("(Built-in role)");
item->setForeground(Qt::darkGray);
}
item->setToolTip(tooltip);
}
}
void AgentRolesWidget::updateButtons()
{
QListWidgetItem *selectedItem = m_rolesList->currentItem();
bool hasSelection = selectedItem != nullptr;
bool isBuiltin = false;
if (hasSelection) {
QString roleId = selectedItem->data(Qt::UserRole).toString();
AgentRole role = AgentRolesManager::loadRole(roleId);
isBuiltin = role.isBuiltin;
}
m_editButton->setEnabled(hasSelection);
m_duplicateButton->setEnabled(hasSelection);
m_deleteButton->setEnabled(hasSelection && !isBuiltin);
}
void AgentRolesWidget::onAddRole()
{
AgentRoleDialog dialog(this);
if (dialog.exec() != QDialog::Accepted)
return;
AgentRole newRole = dialog.getRole();
if (AgentRolesManager::roleExists(newRole.id)) {
QMessageBox::warning(
this,
Tr::tr("Role Already Exists"),
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
.arg(newRole.id));
return;
}
if (AgentRolesManager::saveRole(newRole)) {
loadRoles();
} else {
QMessageBox::critical(
this, Tr::tr("Error"), Tr::tr("Failed to save role '%1'.").arg(newRole.name));
}
}
void AgentRolesWidget::onEditRole()
{
QListWidgetItem *selectedItem = m_rolesList->currentItem();
if (!selectedItem)
return;
QString roleId = selectedItem->data(Qt::UserRole).toString();
AgentRole role = AgentRolesManager::loadRole(roleId);
if (role.isBuiltin) {
QMessageBox::information(
this,
Tr::tr("Cannot Edit Built-in Role"),
Tr::tr(
"Built-in roles cannot be edited. You can duplicate this role and modify the copy."));
return;
}
AgentRoleDialog dialog(role, this);
if (dialog.exec() != QDialog::Accepted)
return;
AgentRole updatedRole = dialog.getRole();
if (AgentRolesManager::saveRole(updatedRole)) {
loadRoles();
} else {
QMessageBox::critical(
this, Tr::tr("Error"), Tr::tr("Failed to update role '%1'.").arg(updatedRole.name));
}
}
void AgentRolesWidget::onDuplicateRole()
{
QListWidgetItem *selectedItem = m_rolesList->currentItem();
if (!selectedItem)
return;
QString roleId = selectedItem->data(Qt::UserRole).toString();
AgentRole role = AgentRolesManager::loadRole(roleId);
role.name += " (Copy)";
role.id += "_copy";
role.isBuiltin = false;
int counter = 1;
QString baseId = role.id;
while (AgentRolesManager::roleExists(role.id)) {
role.id = baseId + QString::number(counter++);
}
AgentRoleDialog dialog(role, false, this);
if (dialog.exec() != QDialog::Accepted)
return;
AgentRole newRole = dialog.getRole();
if (AgentRolesManager::roleExists(newRole.id)) {
QMessageBox::warning(
this,
Tr::tr("Role Already Exists"),
Tr::tr("A role with ID '%1' already exists. Please use a different ID.")
.arg(newRole.id));
return;
}
if (AgentRolesManager::saveRole(newRole)) {
loadRoles();
} else {
QMessageBox::critical(this, Tr::tr("Error"), Tr::tr("Failed to duplicate role."));
}
}
void AgentRolesWidget::onDeleteRole()
{
QListWidgetItem *selectedItem = m_rolesList->currentItem();
if (!selectedItem)
return;
QString roleId = selectedItem->data(Qt::UserRole).toString();
AgentRole role = AgentRolesManager::loadRole(roleId);
if (role.isBuiltin) {
QMessageBox::information(
this, Tr::tr("Cannot Delete Built-in Role"), Tr::tr("Built-in roles cannot be deleted."));
return;
}
QMessageBox::StandardButton reply = QMessageBox::question(
this,
Tr::tr("Delete Role"),
Tr::tr("Are you sure you want to delete the role '%1'?").arg(role.name),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
if (AgentRolesManager::deleteRole(roleId)) {
loadRoles();
} else {
QMessageBox::critical(
this, Tr::tr("Error"), Tr::tr("Failed to delete role '%1'.").arg(role.name));
}
}
}
void AgentRolesWidget::onOpenRolesFolder()
{
QDesktopServices::openUrl(QUrl::fromLocalFile(AgentRolesManager::getConfigurationDirectory()));
}
} // namespace QodeAssist::Settings

View File

@ -0,0 +1,54 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QWidget>
class QListWidget;
class QPushButton;
namespace QodeAssist::Settings {
class AgentRolesWidget : public QWidget
{
Q_OBJECT
public:
explicit AgentRolesWidget(QWidget *parent = nullptr);
private:
void setupUI();
void loadRoles();
void updateButtons();
void onAddRole();
void onEditRole();
void onDuplicateRole();
void onDeleteRole();
void onOpenRolesFolder();
QListWidget *m_rolesList = nullptr;
QPushButton *m_addButton = nullptr;
QPushButton *m_editButton = nullptr;
QPushButton *m_duplicateButton = nullptr;
QPushButton *m_deleteButton = nullptr;
};
} // namespace QodeAssist::Settings

View File

@ -16,11 +16,15 @@ add_library(QodeAssistSettings STATIC
ProviderSettings.hpp ProviderSettings.cpp ProviderSettings.hpp ProviderSettings.cpp
PluginUpdater.hpp PluginUpdater.cpp PluginUpdater.hpp PluginUpdater.cpp
UpdateDialog.hpp UpdateDialog.cpp UpdateDialog.hpp UpdateDialog.cpp
AgentRole.hpp AgentRole.cpp
AgentRoleDialog.hpp AgentRoleDialog.cpp
AgentRolesWidget.hpp AgentRolesWidget.cpp
) )
target_link_libraries(QodeAssistSettings target_link_libraries(QodeAssistSettings
PUBLIC PUBLIC
Qt::Core Qt::Core
Qt::Widgets
Qt::Network Qt::Network
QtCreator::Core QtCreator::Core
QtCreator::Utils QtCreator::Utils

View File

@ -29,6 +29,7 @@
#include "SettingsConstants.hpp" #include "SettingsConstants.hpp"
#include "SettingsTr.hpp" #include "SettingsTr.hpp"
#include "SettingsUtils.hpp" #include "SettingsUtils.hpp"
#include "AgentRolesWidget.hpp"
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
@ -262,6 +263,9 @@ ChatAssistantSettings::ChatAssistantSettings()
chatRenderer.setDefaultValue("rhi"); chatRenderer.setDefaultValue("rhi");
#endif #endif
lastUsedRoleId.setSettingsKey(Constants::CA_LAST_USED_ROLE);
lastUsedRoleId.setDefaultValue("");
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS; resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
readSettings(); readSettings();
@ -405,4 +409,18 @@ public:
const ChatAssistantSettingsPage chatAssistantSettingsPage; const ChatAssistantSettingsPage chatAssistantSettingsPage;
class AgentRolesSettingsPage : public Core::IOptionsPage
{
public:
AgentRolesSettingsPage()
{
setId("QodeAssist.AgentRoles");
setDisplayName(Tr::tr("Agent Roles"));
setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
setWidgetCreator([]() { return new AgentRolesWidget(); });
}
};
const AgentRolesSettingsPage agentRolesSettingsPage;
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@ -21,6 +21,7 @@
#include <utils/aspects.h> #include <utils/aspects.h>
#include "AgentRole.hpp"
#include "ButtonAspect.hpp" #include "ButtonAspect.hpp"
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
@ -82,6 +83,8 @@ public:
Utils::SelectionAspect chatRenderer{this}; Utils::SelectionAspect chatRenderer{this};
Utils::StringAspect lastUsedRoleId{this};
private: private:
void setupConnections(); void setupConnections();
void resetSettingsToDefaults(); void resetSettingsToDefaults();

View File

@ -209,6 +209,8 @@ const char CA_CODE_FONT_SIZE[] = "QodeAssist.caCodeFontSize";
const char CA_TEXT_FORMAT[] = "QodeAssist.caTextFormat"; const char CA_TEXT_FORMAT[] = "QodeAssist.caTextFormat";
const char CA_CHAT_RENDERER[] = "QodeAssist.caChatRenderer"; const char CA_CHAT_RENDERER[] = "QodeAssist.caChatRenderer";
const char CA_LAST_USED_ROLE[] = "QodeAssist.caLastUsedRole";
// quick refactor preset prompt settings // quick refactor preset prompt settings
const char QR_TEMPERATURE[] = "QodeAssist.qrTemperature"; const char QR_TEMPERATURE[] = "QodeAssist.qrTemperature";
const char QR_MAX_TOKENS[] = "QodeAssist.qrMaxTokens"; const char QR_MAX_TOKENS[] = "QodeAssist.qrMaxTokens";