feat: Add chat-agent switcher in chat ui (#247)

* feat: Add chat-agent switcher in chat ui

fix: qml errors

refactor: Change top bar layout

fix: default value

* fix: update github action for qtc
This commit is contained in:
Petr Mironychev
2025-10-31 16:09:38 +01:00
committed by GitHub
parent 9117572f82
commit db82fb08e8
27 changed files with 244 additions and 140 deletions

View File

@ -49,6 +49,7 @@ ChatRootView::ChatRootView(QQuickItem *parent)
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_isRequestInProgress(false)
, m_isAgentMode(false)
{
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect(
@ -142,12 +143,20 @@ ChatRootView::ChatRootView(QQuickItem *parent)
updateInputTokensCount();
refreshRules();
// Refresh rules when project changes
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
QSettings appSettings;
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", true).toBool();
connect(
&Settings::generalSettings().useTools,
&Utils::BaseAspect::changed,
this,
&ChatRootView::toolsSupportEnabledChanged);
}
ChatModel *ChatRootView::chatModel() const
@ -173,7 +182,7 @@ void ChatRootView::sendMessage(const QString &message)
}
}
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles, m_isAgentMode);
clearAttachmentFiles();
setRequestProgressStatus(true);
}
@ -704,4 +713,26 @@ void ChatRootView::refreshRules()
emit activeRulesCountChanged();
}
bool ChatRootView::isAgentMode() const
{
return m_isAgentMode;
}
void ChatRootView::setIsAgentMode(bool newIsAgentMode)
{
if (m_isAgentMode != newIsAgentMode) {
m_isAgentMode = newIsAgentMode;
QSettings settings;
settings.setValue("QodeAssist/Chat/AgentMode", newIsAgentMode);
emit isAgentModeChanged();
}
}
bool ChatRootView::toolsSupportEnabled() const
{
return Settings::generalSettings().useTools();
}
} // namespace QodeAssist::Chat

View File

@ -47,6 +47,9 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
Q_PROPERTY(
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
QML_ELEMENT
@ -107,6 +110,10 @@ public:
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
bool isAgentMode() const;
void setIsAgentMode(bool newIsAgentMode);
bool toolsSupportEnabled() const;
public slots:
void sendMessage(const QString &message);
void copyToClipboard(const QString &text);
@ -134,6 +141,9 @@ signals:
void activeRulesChanged();
void activeRulesCountChanged();
void isAgentModeChanged();
void toolsSupportEnabledChanged();
private:
QString getChatsHistoryDir() const;
QString getSuggestedFileName() const;
@ -152,6 +162,7 @@ private:
bool m_isRequestInProgress;
QString m_lastErrorMessage;
QVariantList m_activeRules;
bool m_isAgentMode;
};
} // namespace QodeAssist::Chat

View File

@ -55,7 +55,10 @@ ClientInterface::ClientInterface(
ClientInterface::~ClientInterface() = default;
void ClientInterface::sendMessage(
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
const QString &message,
const QList<QString> &attachments,
const QList<QString> &linkedFiles,
bool useAgentMode)
{
cancelRequest();
m_accumulatedResponses.clear();
@ -83,10 +86,12 @@ void ClientInterface::sendMessage(
LLMCore::ContextData context;
const bool isToolsEnabled = Settings::generalSettings().useTools() && useAgentMode;
if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt();
if (Settings::generalSettings().useTools()) {
if (isToolsEnabled) {
systemPrompt += "\n\n# Tool Usage Guidelines\n\n"
"**Multi-tool workflows:**\n"
"- Code structure: search_project (symbol mode) → find_and_read_file\n"
@ -140,8 +145,8 @@ void ClientInterface::sendMessage(
config.apiKey = provider->apiKey();
config.provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
config.provider->prepareRequest(
config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat, isToolsEnabled);
QString requestId = QUuid::createUuid().toString();
QJsonObject request{{"id", requestId}};

View File

@ -42,7 +42,8 @@ public:
void sendMessage(
const QString &message,
const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {});
const QList<QString> &linkedFiles = {},
bool useAgentMode = false);
void clearMessages();
void cancelRequest();

View File

@ -65,7 +65,7 @@ ChatRootView {
id: topBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
Layout.preferredHeight: childrenRect.height + 10
saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog()
@ -74,7 +74,7 @@ ChatRootView {
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
}
recentPath {
text: qsTr("Latest chat file 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()
rulesButton.onClicked: rulesViewer.open()
@ -84,6 +84,13 @@ ChatRootView {
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
}
agentModeSwitch {
checked: root.isAgentMode
enabled: root.toolsSupportEnabled
onCheckedChanged: {
root.isAgentMode = agentModeSwitch.checked
}
}
}
ListView {
@ -108,7 +115,7 @@ ChatRootView {
if (model.roleType === ChatModel.Tool) {
return toolMessageComponent
} else if (model.roleType === ChatModel.FileEdit) {
return fileEditSuggestionComponent
return toolMessageComponent
} else {
return chatItemComponent
}

View File

@ -34,21 +34,20 @@ Rectangle {
property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId
property alias rulesButton: rulesButtonId
property alias agentModeSwitch: agentModeSwitchId
property alias activeRulesCount: activeRulesCountId.text
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
Flow {
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
margins: 5
}
spacing: 10
QoAButton {
@ -69,107 +68,144 @@ Rectangle {
: qsTr("Pin chat window to the top")
}
QoAButton {
id: saveButtonId
QoATextSlider {
id: agentModeSwitchId
icon {
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Save chat to *.json file")
}
QoAButton {
id: loadButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Load chat from *.json file")
}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
Text {
id: recentPathId
elide: Text.ElideMiddle
color: palette.text
}
QoAButton {
id: openChatHistoryId
icon {
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Show in system")
}
QoAButton {
id: rulesButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
height: 15
width: 15
}
text: " "
leftText: "chat"
rightText: "AI Agent"
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: root.activeRulesCount > 0
? 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
ToolTip.text: {
if (!agentModeSwitchId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
color: palette.text
font.pixelSize: 10
font.bold: true
return checked
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
: qsTr("Chat Mode: Simple conversation without tool access")
}
}
Item {
Layout.fillWidth: true
height: agentModeSwitchId.height
width: recentPathId.width
Text {
id: recentPathId
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, root.width)
elide: Text.ElideMiddle
color: palette.text
font.pixelSize: 12
MouseArea {
anchors.fill: parent
hoverEnabled: true
ToolTip.visible: containsMouse
ToolTip.delay: 500
ToolTip.text: recentPathId.text
}
}
}
Badge {
id: tokensBadgeId
RowLayout {
Layout.preferredWidth: root.width
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
spacing: 10
QoAButton {
id: saveButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Save chat to *.json file")
}
QoAButton {
id: loadButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Load chat from *.json file")
}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
QoAButton {
id: openChatHistoryId
icon {
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Show in system")
}
QoAButton {
id: rulesButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
height: 15
width: 15
}
text: " "
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: root.activeRulesCount > 0
? 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 {
id: tokensBadgeId
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
}
}
}
}