diff --git a/CMakeLists.txt b/CMakeLists.txt index f1956b8..d406c7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,10 +2,6 @@ cmake_minimum_required(VERSION 3.16) project(QodeAssist) -option(QODEASSIST_EXPERIMENTAL - "Enable experimental features" OFF) -message(STATUS "QodeAssist experimental features: ${QODEASSIST_EXPERIMENTAL}") - set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) @@ -70,8 +66,6 @@ add_qtc_plugin(QodeAssist QtCreator::CPlusPlus LLMQore PluginLLMCore - ProvidersConfig - Agents Skills QodeAssistChatViewplugin SOURCES @@ -170,11 +164,6 @@ add_qtc_plugin(QodeAssist settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp ) -if(QODEASSIST_EXPERIMENTAL) - target_compile_definitions(QodeAssist PRIVATE QODEASSIST_EXPERIMENTAL) - target_link_libraries(QodeAssist PRIVATE QodeAssistAgentPipelines) -endif() - get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) find_program(QtCreatorExecutable NAMES diff --git a/TaskFlow/CMakeLists.txt b/TaskFlow/CMakeLists.txt deleted file mode 100644 index cebfdf7..0000000 --- a/TaskFlow/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -add_subdirectory(core) -add_subdirectory(Editor) -# add_subdirectory(serialization) -# add_subdirectory(tasks) - -qt_add_library(TaskFlow STATIC) - -target_link_libraries(TaskFlow - PUBLIC - TaskFlowCore - TaskFlowEditorplugin - # TaskFlowSerialization - # TaskFlowTasks -) diff --git a/TaskFlow/Editor/CMakeLists.txt b/TaskFlow/Editor/CMakeLists.txt deleted file mode 100644 index be451cf..0000000 --- a/TaskFlow/Editor/CMakeLists.txt +++ /dev/null @@ -1,41 +0,0 @@ -qt_add_library(TaskFlowEditor STATIC) - -qt_policy(SET QTP0001 NEW) -qt_policy(SET QTP0004 NEW) - -qt_add_qml_module(TaskFlowEditor - URI TaskFlow.Editor - VERSION 1.0 - DEPENDENCIES QtQuick - RESOURCES - QML_FILES - qml/FlowEditorView.qml - qml/Flow.qml - qml/Task.qml - qml/TaskPort.qml - qml/TaskParameter.qml - qml/TaskConnection.qml - SOURCES - FlowEditor.hpp FlowEditor.cpp - FlowsModel.hpp FlowsModel.cpp - TaskItem.hpp TaskItem.cpp - FlowItem.hpp FlowItem.cpp - TaskModel.hpp TaskModel.cpp - TaskPortItem.hpp TaskPortItem.cpp - TaskPortModel.hpp TaskPortModel.cpp - TaskConnectionsModel.hpp TaskConnectionsModel.cpp - TaskConnectionItem.hpp TaskConnectionItem.cpp - GridBackground.hpp GridBackground.cpp -) - -target_link_libraries(TaskFlowEditor - PUBLIC - Qt::Quick - PRIVATE - TaskFlowCore -) - -target_include_directories(TaskFlowEditor - PUBLIC - ${CMAKE_CURRENT_LIST_DIR} -) diff --git a/TaskFlow/Editor/FlowEditor.cpp b/TaskFlow/Editor/FlowEditor.cpp deleted file mode 100644 index f825f70..0000000 --- a/TaskFlow/Editor/FlowEditor.cpp +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "FlowEditor.hpp" - -namespace QodeAssist::TaskFlow { - -FlowEditor::FlowEditor(QQuickItem *parent) - : QQuickItem(parent) -{} - -void FlowEditor::initialize() -{ - emit availableTaskTypesChanged(); - emit availableFlowsChanged(); - - m_flowsModel = new FlowsModel(m_flowManager, this); - - emit flowsModelChanged(); - - if (m_flowsModel->rowCount() > 0) { - setCurrentFlowIndex(0); - } - - // setCurrentFlowId(m_flowManager->flows().begin().value()->flowId()); - m_currentFlow = m_flowManager->getFlow(); - emit currentFlowChanged(); -} - -QString FlowEditor::currentFlowId() const -{ - return m_currentFlowId; -} - -void FlowEditor::setCurrentFlowId(const QString &newCurrentFlowId) -{ - if (m_currentFlowId == newCurrentFlowId) - return; - m_currentFlowId = newCurrentFlowId; - emit currentFlowIdChanged(); -} - -QStringList FlowEditor::availableTaskTypes() const -{ - if (m_flowManager) - return m_flowManager->getAvailableTasksTypes(); - else { - return {"No flow manager"}; - } -} - -QStringList FlowEditor::availableFlows() const -{ - if (m_flowManager) { - auto flows = m_flowManager->getAvailableFlows(); - return flows.size() > 0 ? flows : QStringList{"No flows"}; - } else { - return {"No flow manager"}; - } -} - -void FlowEditor::setFlowManager(FlowManager *newFlowManager) -{ - if (m_flowManager == newFlowManager) - return; - m_flowManager = newFlowManager; - - initialize(); -} - -FlowsModel *FlowEditor::flowsModel() const -{ - return m_flowsModel; -} - -int FlowEditor::currentFlowIndex() const -{ - return m_currentFlowIndex; -} - -void FlowEditor::setCurrentFlowIndex(int newCurrentFlowIndex) -{ - if (m_currentFlowIndex == newCurrentFlowIndex) - return; - m_currentFlowIndex = newCurrentFlowIndex; - emit currentFlowIndexChanged(); -} - -Flow *FlowEditor::getFlow(const QString &flowName) -{ - return m_flowManager->getFlow(flowName); -} - -Flow *FlowEditor::getCurrentFlow() -{ - return m_flowManager->getFlow(m_currentFlowId); -} - -Flow *FlowEditor::currentFlow() const -{ - return m_currentFlow; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/FlowEditor.hpp b/TaskFlow/Editor/FlowEditor.hpp deleted file mode 100644 index 4e09b15..0000000 --- a/TaskFlow/Editor/FlowEditor.hpp +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include "FlowsModel.hpp" -#include - -namespace QodeAssist::TaskFlow { - -class FlowEditor : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY( - QString currentFlowId READ currentFlowId WRITE setCurrentFlowId NOTIFY currentFlowIdChanged) - Q_PROPERTY( - QStringList availableTaskTypes READ availableTaskTypes NOTIFY availableTaskTypesChanged) - Q_PROPERTY(QStringList availableFlows READ availableFlows NOTIFY availableFlowsChanged) - Q_PROPERTY(FlowsModel *flowsModel READ flowsModel NOTIFY flowsModelChanged) - Q_PROPERTY(int currentFlowIndex READ currentFlowIndex WRITE setCurrentFlowIndex NOTIFY - currentFlowIndexChanged) - - Q_PROPERTY(Flow *currentFlow READ currentFlow NOTIFY currentFlowChanged FINAL) - -public: - FlowEditor(QQuickItem *parent = nullptr); - - void initialize(); - - QString currentFlowId() const; - void setCurrentFlowId(const QString &newCurrentFlowId); - - QStringList availableTaskTypes() const; - QStringList availableFlows() const; - - void setFlowManager(FlowManager *newFlowManager); - - FlowsModel *flowsModel() const; - - int currentFlowIndex() const; - void setCurrentFlowIndex(int newCurrentFlowIndex); - - Q_INVOKABLE Flow *getFlow(const QString &flowName); - Q_INVOKABLE Flow *getCurrentFlow(); - - Flow *currentFlow() const; - -signals: - void currentFlowIdChanged(); - void availableTaskTypesChanged(); - void availableFlowsChanged(); - void flowsModelChanged(); - - void currentFlowIndexChanged(); - - void currentFlowChanged(); - -private: - FlowManager *m_flowManager = nullptr; - QString m_currentFlowId; - FlowsModel *m_flowsModel; - int m_currentFlowIndex; - Flow *m_currentFlow = nullptr; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/FlowItem.cpp b/TaskFlow/Editor/FlowItem.cpp deleted file mode 100644 index 7df0b46..0000000 --- a/TaskFlow/Editor/FlowItem.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "FlowItem.hpp" - -namespace QodeAssist::TaskFlow { - -FlowItem::FlowItem(QQuickItem *parent) - : QQuickItem(parent) -{ - connect(this, &QQuickItem::childrenChanged, this, [this]() { updateFlowLayout(); }); -} - -QString FlowItem::flowId() const -{ - if (!m_flow) - return {"no flow"}; - return m_flow->flowId(); -} - -void FlowItem::setFlowId(const QString &newFlowId) -{ - if (m_flow->flowId() == newFlowId) - return; - m_flow->setFlowId(newFlowId); - emit flowIdChanged(); -} - -Flow *FlowItem::flow() const -{ - return m_flow; -} - -void FlowItem::setFlow(Flow *newFlow) -{ - if (m_flow == newFlow) - return; - m_flow = newFlow; - emit flowChanged(); - emit flowIdChanged(); - qDebug() << "FlowItem::setFlow" << m_flow->flowId() << newFlow; - - m_taskModel = new TaskModel(m_flow, this); - m_connectionsModel = new TaskConnectionsModel(m_flow, this); - - emit taskModelChanged(); - emit connectionsModelChanged(); -} - -TaskModel *FlowItem::taskModel() const -{ - return m_taskModel; -} - -TaskConnectionsModel *FlowItem::connectionsModel() const -{ - return m_connectionsModel; -} - -QVariantList FlowItem::taskItems() const -{ - return m_taskItems; -} - -void FlowItem::setTaskItems(const QVariantList &newTaskItems) -{ - qDebug() << "FlowItem::setTaskItems" << newTaskItems; - if (m_taskItems == newTaskItems) - return; - m_taskItems = newTaskItems; - emit taskItemsChanged(); -} - -void FlowItem::updateFlowLayout() -{ - auto allItems = this->childItems(); - - for (auto child : allItems) { - if (child->objectName() == QString("TaskItem")) { - qDebug() << "Found TaskItem:" << child; - auto taskItem = qobject_cast(child); - m_taskItemsList.insert(taskItem, taskItem->task()); - } - - if (child->objectName() == QString("TaskConnectionItem")) { - qDebug() << "Found TaskConnectionItem:" << child; - auto connectionItem = qobject_cast(child); - m_taskConnectionsList.insert(connectionItem, connectionItem->connection()); - } - } -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/FlowItem.hpp b/TaskFlow/Editor/FlowItem.hpp deleted file mode 100644 index 6f4f7df..0000000 --- a/TaskFlow/Editor/FlowItem.hpp +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include "TaskConnectionItem.hpp" -#include "TaskConnectionsModel.hpp" -#include "TaskItem.hpp" -#include "TaskModel.hpp" -#include -#include - -namespace QodeAssist::TaskFlow { - -class FlowItem : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(QString flowId READ flowId WRITE setFlowId NOTIFY flowIdChanged) - Q_PROPERTY(Flow *flow READ flow WRITE setFlow NOTIFY flowChanged) - Q_PROPERTY(TaskModel *taskModel READ taskModel NOTIFY taskModelChanged) - Q_PROPERTY( - TaskConnectionsModel *connectionsModel READ connectionsModel NOTIFY connectionsModelChanged) - Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged) - -public: - explicit FlowItem(QQuickItem *parent = nullptr); - - QString flowId() const; - void setFlowId(const QString &newFlowId); - - Flow *flow() const; - void setFlow(Flow *newFlow); - - TaskModel *taskModel() const; - - TaskConnectionsModel *connectionsModel() const; - - QVariantList taskItems() const; - void setTaskItems(const QVariantList &newTaskItems); - - void updateFlowLayout(); - -signals: - void flowIdChanged(); - void flowChanged(); - void taskModelChanged(); - void connectionsModelChanged(); - void taskItemsChanged(); - -private: - Flow *m_flow = nullptr; - TaskModel *m_taskModel = nullptr; - TaskConnectionsModel *m_connectionsModel = nullptr; - QVariantList m_taskItems; - - QHash m_taskItemsList; - QHash m_taskConnectionsList; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/FlowsModel.cpp b/TaskFlow/Editor/FlowsModel.cpp deleted file mode 100644 index 1aa82dc..0000000 --- a/TaskFlow/Editor/FlowsModel.cpp +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "FlowsModel.hpp" - -#include "FlowManager.hpp" - -namespace QodeAssist::TaskFlow { - -FlowsModel::FlowsModel(FlowManager *flowManager, QObject *parent) - : QAbstractListModel(parent) - , m_flowManager(flowManager) -{ - connect(m_flowManager, &FlowManager::flowAdded, this, &FlowsModel::onFlowAdded); -} - -int FlowsModel::rowCount(const QModelIndex &parent) const -{ - return m_flowManager->flows().size(); -} - -QVariant FlowsModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || !m_flowManager || index.row() >= m_flowManager->flows().size()) - return QVariant(); - - const auto flows = m_flowManager->flows().values(); - - switch (role) { - case FlowRoles::FlowIdRole: - return flows.at(index.row())->flowId(); - case FlowRoles::FlowDataRole: - return QVariant::fromValue(flows.at(index.row())); - default: - return QVariant(); - } -} - -QHash FlowsModel::roleNames() const -{ - QHash roles; - roles[FlowRoles::FlowIdRole] = "flowId"; - roles[FlowRoles::FlowDataRole] = "flowData"; - return roles; -} - -void FlowsModel::onFlowAdded(const QString &flowId) -{ - // qDebug() << "FlowsModel::Flow added: " << flowId; - // int newIndex = m_flowManager->flows().size(); - // beginInsertRows(QModelIndex(), newIndex, newIndex); - // endInsertRows(); -} - -void FlowsModel::onFlowRemoved(const QString &flowId) {} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/FlowsModel.hpp b/TaskFlow/Editor/FlowsModel.hpp deleted file mode 100644 index 97c6555..0000000 --- a/TaskFlow/Editor/FlowsModel.hpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -// #include "tasks/Flow.hpp" -#include - -namespace QodeAssist::TaskFlow { - -class FlowsModel : public QAbstractListModel -{ - Q_OBJECT -public: - enum FlowRoles { FlowIdRole = Qt::UserRole, FlowDataRole }; - - FlowsModel(FlowManager *flowManager, QObject *parent = nullptr); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; - -public slots: - void onFlowAdded(const QString &flowId); - void onFlowRemoved(const QString &flowId); - -private: - FlowManager *m_flowManager; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/GridBackground.cpp b/TaskFlow/Editor/GridBackground.cpp deleted file mode 100644 index e1f5c3c..0000000 --- a/TaskFlow/Editor/GridBackground.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "GridBackground.hpp" -#include -#include -#include -#include -#include - -namespace QodeAssist::TaskFlow { - -GridBackground::GridBackground(QQuickItem *parent) - : QQuickItem(parent) -{ - setFlag(QQuickItem::ItemHasContents, true); -} - -int GridBackground::gridSize() const -{ - return m_gridSize; -} - -void GridBackground::setGridSize(int size) -{ - if (m_gridSize != size) { - m_gridSize = size; - update(); - emit gridSizeChanged(); - } -} - -QColor GridBackground::gridColor() const -{ - return m_gridColor; -} - -void GridBackground::setGridColor(const QColor &color) -{ - if (m_gridColor != color) { - m_gridColor = color; - update(); - emit gridColorChanged(); - } -} - -QSGNode *GridBackground::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) -{ - QSGSimpleTextureNode *node = static_cast(oldNode); - if (!node) { - node = new QSGSimpleTextureNode(); - } - - QPixmap pixmap(width(), height()); - pixmap.fill(Qt::transparent); - - QPainter painter(&pixmap); - painter.setRenderHint(QPainter::Antialiasing, false); - - QPen pen(m_gridColor); - pen.setWidth(1); - painter.setPen(pen); - painter.setOpacity(this->opacity()); - - for (int x = 0; x < width(); x += m_gridSize) { - painter.drawLine(x, 0, x, height()); - } - - for (int y = 0; y < height(); y += m_gridSize) { - painter.drawLine(0, y, width(), y); - } - - painter.end(); - - QSGTexture *texture = window()->createTextureFromImage(pixmap.toImage()); - node->setTexture(texture); - node->setRect(boundingRect()); - - return node; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/GridBackground.hpp b/TaskFlow/Editor/GridBackground.hpp deleted file mode 100644 index df52b9c..0000000 --- a/TaskFlow/Editor/GridBackground.hpp +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include - -namespace QodeAssist::TaskFlow { - -class GridBackground : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged) - Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged) - -public: - explicit GridBackground(QQuickItem *parent = nullptr); - - int gridSize() const; - void setGridSize(int size); - - QColor gridColor() const; - void setGridColor(const QColor &color); - -signals: - void gridSizeChanged(); - void gridColorChanged(); - -protected: - QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override; - -private: - int m_gridSize = 20; - QColor m_gridColor = QColor(128, 128, 128); -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskConnectionItem.cpp b/TaskFlow/Editor/TaskConnectionItem.cpp deleted file mode 100644 index 585e49d..0000000 --- a/TaskFlow/Editor/TaskConnectionItem.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskConnectionItem.hpp" -#include "TaskItem.hpp" -#include "TaskPortItem.hpp" -#include - -namespace QodeAssist::TaskFlow { - -TaskConnectionItem::TaskConnectionItem(QQuickItem *parent) - : QQuickItem(parent) -{ - setObjectName("TaskConnectionItem"); -} - -void TaskConnectionItem::setConnection(TaskConnection *connection) -{ - if (m_connection == connection) - return; - - m_connection = connection; - emit connectionChanged(); - - calculatePositions(); -} - -void TaskConnectionItem::updatePositions() -{ - // calculatePositions(); -} - -void TaskConnectionItem::calculatePositions() -{ - if (!m_connection) { - return; - } - - // Find source task item - QQuickItem *sourceTaskItem = findTaskItem(m_connection->sourceTask()); - QQuickItem *targetTaskItem = findTaskItem(m_connection->targetTask()); - - if (!sourceTaskItem || !targetTaskItem) { - return; - } - - // Find port items within tasks - QQuickItem *sourcePortItem = findPortItem(sourceTaskItem, m_connection->sourcePort()); - QQuickItem *targetPortItem = findPortItem(targetTaskItem, m_connection->targetPort()); - - if (!sourcePortItem || !targetPortItem) { - return; - } - - // Calculate global positions - QPointF sourceGlobal - = sourcePortItem - ->mapToItem(parentItem(), sourcePortItem->width() / 2, sourcePortItem->height() / 2); - QPointF targetGlobal - = targetPortItem - ->mapToItem(parentItem(), targetPortItem->width() / 2, targetPortItem->height() / 2); - - if (m_startPoint != sourceGlobal) { - m_startPoint = sourceGlobal; - emit startPointChanged(); - } - - if (m_endPoint != targetGlobal) { - m_endPoint = targetGlobal; - emit endPointChanged(); - } -} - -QQuickItem *TaskConnectionItem::findTaskItem(BaseTask *task) -{ - for (const QVariant &item : m_taskItems) { - QQuickItem *taskItem = qvariant_cast(item); - if (!taskItem) - continue; - - QVariant taskProp = taskItem->property("task"); - if (taskProp.isValid() && taskProp.value() == task) { - return taskItem; - } - } - return nullptr; -} - -QQuickItem *TaskConnectionItem::findTaskItemRecursive(QQuickItem *item, BaseTask *task) -{ - // Проверяем objectName и task property - if (item->objectName() == "TaskItem") { - QVariant taskProp = item->property("task"); - if (taskProp.isValid()) { - BaseTask *itemTask = taskProp.value(); - if (itemTask == task) { - return item; - } - } - } - - // Рекурсивно ищем в детях - auto children = item->childItems(); - - for (QQuickItem *child : children) { - if (QQuickItem *found = findTaskItemRecursive(child, task)) { - return found; - } - } - - return nullptr; -} - -QQuickItem *TaskConnectionItem::findPortItem(QQuickItem *taskItem, TaskPort *port) -{ - std::function findPortRecursive = - [&](QQuickItem *item) -> QQuickItem * { - // Проверяем objectName и port property - if (item->objectName() == "TaskPortItem") { - QVariant portProp = item->property("port"); - if (portProp.isValid()) { - TaskPort *itemPort = portProp.value(); - if (itemPort == port) { - return item; - } - } - } - - // Рекурсивно ищем в детях - for (QQuickItem *child : item->childItems()) { - if (QQuickItem *found = findPortRecursive(child)) { - return found; - } - } - return nullptr; - }; - - return findPortRecursive(taskItem); -} - -QVariantList TaskConnectionItem::taskItems() const -{ - return m_taskItems; -} - -void TaskConnectionItem::setTaskItems(const QVariantList &newTaskItems) -{ - if (m_taskItems == newTaskItems) - return; - m_taskItems = newTaskItems; - emit taskItemsChanged(); - - calculatePositions(); -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskConnectionItem.hpp b/TaskFlow/Editor/TaskConnectionItem.hpp deleted file mode 100644 index 96c9f13..0000000 --- a/TaskFlow/Editor/TaskConnectionItem.hpp +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "TaskConnection.hpp" -#include -#include - -namespace QodeAssist::TaskFlow { - -class TaskConnectionItem : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(QPointF startPoint READ startPoint NOTIFY startPointChanged) - Q_PROPERTY(QPointF endPoint READ endPoint NOTIFY endPointChanged) - Q_PROPERTY( - TaskConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged) - - Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged) - -public: - TaskConnectionItem(QQuickItem *parent = nullptr); - - QPointF startPoint() const { return m_startPoint; } - QPointF endPoint() const { return m_endPoint; } - - TaskConnection *connection() const { return m_connection; } - void setConnection(TaskConnection *connection); - - Q_INVOKABLE void updatePositions(); - - QVariantList taskItems() const; - void setTaskItems(const QVariantList &newTaskItems); - -signals: - void startPointChanged(); - void endPointChanged(); - void connectionChanged(); - - void taskItemsChanged(); - -private: - void calculatePositions(); - QQuickItem *findTaskItem(BaseTask *task); - QQuickItem *findTaskItemRecursive(QQuickItem *item, BaseTask *task); - QQuickItem *findPortItem(QQuickItem *taskItem, TaskPort *port); - -private: - TaskConnection *m_connection = nullptr; - QPointF m_startPoint; - QPointF m_endPoint; - QVariantList m_taskItems; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskConnectionsModel.cpp b/TaskFlow/Editor/TaskConnectionsModel.cpp deleted file mode 100644 index 0abea93..0000000 --- a/TaskFlow/Editor/TaskConnectionsModel.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskConnectionsModel.hpp" - -namespace QodeAssist::TaskFlow { - -TaskConnectionsModel::TaskConnectionsModel(Flow *flow, QObject *parent) - : QAbstractListModel(parent) - , m_flow(flow) -{} - -int TaskConnectionsModel::rowCount(const QModelIndex &parent) const -{ - return m_flow->connections().size(); -} - -QVariant TaskConnectionsModel::data(const QModelIndex &index, int role) const -{ - if (role == TaskConnectionsRoles::TaskConnectionsRole) - return QVariant::fromValue(m_flow->connections().at(index.row())); - return QVariant(); -} - -QHash TaskConnectionsModel::roleNames() const -{ - QHash roles; - roles[TaskConnectionsRoles::TaskConnectionsRole] = "connectionData"; - return roles; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskConnectionsModel.hpp b/TaskFlow/Editor/TaskConnectionsModel.hpp deleted file mode 100644 index 9a1d814..0000000 --- a/TaskFlow/Editor/TaskConnectionsModel.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -#include - -namespace QodeAssist::TaskFlow { - -class TaskConnectionsModel : public QAbstractListModel -{ -public: - enum TaskConnectionsRoles { TaskConnectionsRole = Qt::UserRole }; - - explicit TaskConnectionsModel(Flow *flow, QObject *parent = nullptr); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; - -private: - Flow *m_flow; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskItem.cpp b/TaskFlow/Editor/TaskItem.cpp deleted file mode 100644 index c92bf42..0000000 --- a/TaskFlow/Editor/TaskItem.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskItem.hpp" - -namespace QodeAssist::TaskFlow { - -TaskItem::TaskItem(QQuickItem *parent) - : QQuickItem(parent) -{ - setObjectName("TaskItem"); -} - -QString TaskItem::taskId() const -{ - return m_taskId; -} - -void TaskItem::setTaskId(const QString &newTaskId) -{ - if (m_taskId == newTaskId) - return; - m_taskId = newTaskId; - emit taskIdChanged(); -} - -QString TaskItem::taskType() const -{ - return m_task ? m_task->taskType() : QString(); -} - -BaseTask *TaskItem::task() const -{ - return m_task; -} - -void TaskItem::setTask(BaseTask *newTask) -{ - if (m_task == newTask) - return; - - m_task = newTask; - - if (m_task) { - m_taskId = m_task->taskId(); - - // Обновляем модели портов - m_inputPorts = new TaskPortModel(m_task->getInputPorts(), this); - m_outputPorts = new TaskPortModel(m_task->getOutputPorts(), this); - } else { - m_inputPorts = nullptr; - m_outputPorts = nullptr; - } - - emit taskChanged(); - emit inputPortsChanged(); - emit outputPortsChanged(); - emit taskIdChanged(); - emit taskTypeChanged(); -} - -TaskPortModel *TaskItem::inputPorts() const -{ - return m_inputPorts; -} - -TaskPortModel *TaskItem::outputPorts() const -{ - return m_outputPorts; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskItem.hpp b/TaskFlow/Editor/TaskItem.hpp deleted file mode 100644 index a775067..0000000 --- a/TaskFlow/Editor/TaskItem.hpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include "TaskPortModel.hpp" -#include -#include - -namespace QodeAssist::TaskFlow { - -class TaskItem : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(QString taskId READ taskId WRITE setTaskId NOTIFY taskIdChanged) - Q_PROPERTY(QString taskType READ taskType NOTIFY taskTypeChanged) - Q_PROPERTY(BaseTask *task READ task WRITE setTask NOTIFY taskChanged) - Q_PROPERTY(TaskPortModel *inputPorts READ inputPorts NOTIFY inputPortsChanged) - Q_PROPERTY(TaskPortModel *outputPorts READ outputPorts NOTIFY outputPortsChanged) - -public: - TaskItem(QQuickItem *parent = nullptr); - - QString taskId() const; - void setTaskId(const QString &newTaskId); - QString taskType() const; - - BaseTask *task() const; - void setTask(BaseTask *newTask); - - TaskPortModel *inputPorts() const; - TaskPortModel *outputPorts() const; - -signals: - void taskIdChanged(); - void taskTypeChanged(); - void taskChanged(); - void inputPortsChanged(); - void outputPortsChanged(); - -private: - QString m_taskId; - BaseTask *m_task = nullptr; - TaskPortModel *m_inputPorts = nullptr; - TaskPortModel *m_outputPorts = nullptr; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskModel.cpp b/TaskFlow/Editor/TaskModel.cpp deleted file mode 100644 index 5fc36a1..0000000 --- a/TaskFlow/Editor/TaskModel.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskModel.hpp" - -namespace QodeAssist::TaskFlow { - -TaskModel::TaskModel(Flow *flow, QObject *parent) - : QAbstractListModel(parent) - , m_flow(flow) -{} - -int TaskModel::rowCount(const QModelIndex &parent) const -{ - return m_flow->tasks().size(); -} - -QVariant TaskModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || !m_flow || index.row() >= m_flow->tasks().size()) - return QVariant(); - - const auto &task = m_flow->tasks().values(); - - switch (role) { - case TaskRoles::TaskIdRole: - return task.at(index.row())->taskId(); - case TaskRoles::TaskDataRole: - return QVariant::fromValue(task.at(index.row())); - default: - return QVariant(); - } -} - -QHash TaskModel::roleNames() const -{ - QHash roles; - roles[TaskRoles::TaskIdRole] = "taskId"; - roles[TaskRoles::TaskDataRole] = "taskData"; - return roles; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskModel.hpp b/TaskFlow/Editor/TaskModel.hpp deleted file mode 100644 index b9b74d5..0000000 --- a/TaskFlow/Editor/TaskModel.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include - -namespace QodeAssist::TaskFlow { - -class TaskModel : public QAbstractListModel -{ - Q_OBJECT -public: - enum TaskRoles { TaskIdRole = Qt::UserRole, TaskDataRole }; - - TaskModel(Flow *flow, QObject *parent); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; - -private: - Flow *m_flow; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskPortItem.cpp b/TaskFlow/Editor/TaskPortItem.cpp deleted file mode 100644 index 5f97f13..0000000 --- a/TaskFlow/Editor/TaskPortItem.cpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskPortItem.hpp" - -namespace QodeAssist::TaskFlow { - -TaskPortItem::TaskPortItem(QQuickItem *parent) - : QQuickItem(parent) -{ - setObjectName("TaskPortItem"); -} - -TaskPort *TaskPortItem::port() const -{ - return m_port; -} - -void TaskPortItem::setPort(TaskPort *newPort) -{ - if (m_port == newPort) - return; - m_port = newPort; - emit portChanged(); -} - -QString TaskPortItem::name() const -{ - return m_port->name(); -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskPortItem.hpp b/TaskFlow/Editor/TaskPortItem.hpp deleted file mode 100644 index 2ae6228..0000000 --- a/TaskFlow/Editor/TaskPortItem.hpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::TaskFlow { - -class TaskPortItem : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(TaskPort *port READ port WRITE setPort NOTIFY portChanged) - Q_PROPERTY(QString name READ name CONSTANT) - -public: - TaskPortItem(QQuickItem *parent = nullptr); - - TaskPort *port() const; - void setPort(TaskPort *newPort); - - QString name() const; - -signals: - void portChanged(); - -private: - TaskPort *m_port = nullptr; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskPortModel.cpp b/TaskFlow/Editor/TaskPortModel.cpp deleted file mode 100644 index abdd6f1..0000000 --- a/TaskFlow/Editor/TaskPortModel.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskPortModel.hpp" -#include "TaskPort.hpp" - -namespace QodeAssist::TaskFlow { - -TaskPortModel::TaskPortModel(const QList &ports, QObject *parent) - : QAbstractListModel(parent) - , m_ports(ports) -{} - -int TaskPortModel::rowCount(const QModelIndex &parent) const -{ - return m_ports.size(); -} - -QVariant TaskPortModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || index.row() >= m_ports.size()) - return QVariant(); - - switch (role) { - case TaskPortRoles::TaskPortNameRole: - return m_ports.at(index.row())->name(); - case TaskPortRoles::TaskPortDataRole: - return QVariant::fromValue(m_ports.at(index.row())); - default: - return QVariant(); - } -} - -QHash TaskPortModel::roleNames() const -{ - QHash roles; - roles[TaskPortRoles::TaskPortNameRole] = "taskPortName"; - roles[TaskPortRoles::TaskPortDataRole] = "taskPortData"; - return roles; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/TaskPortModel.hpp b/TaskFlow/Editor/TaskPortModel.hpp deleted file mode 100644 index 4bd6012..0000000 --- a/TaskFlow/Editor/TaskPortModel.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include - -namespace QodeAssist::TaskFlow { - -class TaskPortModel : public QAbstractListModel -{ - Q_OBJECT -public: - enum TaskPortRoles { TaskPortNameRole = Qt::UserRole, TaskPortDataRole }; - - TaskPortModel(const QList &ports, QObject *parent = nullptr); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; - -private: - QList m_ports; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/Editor/qml/Flow.qml b/TaskFlow/Editor/qml/Flow.qml deleted file mode 100644 index 9007b13..0000000 --- a/TaskFlow/Editor/qml/Flow.qml +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -import QtQuick -import TaskFlow.Editor - -FlowItem { - id: root - - Repeater { - id: tasks - - model: root.taskModel - delegate: Task { - // task: taskData - } - } - - Repeater { - id: connections - - model: root.taskModel - delegate: TaskConnection { - // task: taskData - } - } - - // property var qtaskItems: [] - - // // Flow container background - // Rectangle { - // anchors.fill: parent - // color: palette.alternateBase - // border.color: palette.mid - // border.width: 2 - // radius: 8 - - // // Flow header - // Rectangle { - // id: flowHeader - // anchors.top: parent.top - // anchors.left: parent.left - // anchors.right: parent.right - // height: 40 - // color: palette.button - // radius: 6 - - // Rectangle { - // anchors.bottom: parent.bottom - // anchors.left: parent.left - // anchors.right: parent.right - // height: parent.radius - // color: parent.color - // } - - // Text { - // anchors.centerIn: parent - // text: root.flowId - // color: palette.buttonText - // font.pixelSize: 14 - // font.bold: true - // } - // } - - // // // Tasks container - // // Row { - // // id: tasksRow - // // anchors.top: flowHeader.bottom - // // anchors.left: parent.left - // // anchors.margins: 25 - // // anchors.topMargin: 25 - // // objectName: "FlowTaskRow" - - // // spacing: 40 - - // // Repeater { - // // model: root.taskModel - - // // delegate: Task { - // // task: taskData - // // } - - // // onItemAdded: function(index, item){ - // // console.log("task added", index, item) - // // qtaskItems.push(item) - // // root.insertTaskItem(index, item) - // // } - - // // onItemRemoved: function(index, item){ - // // console.log("task added", index, item) - // // var idx = qtaskItems.indexOf(item) - // // if (idx !== -1) qtaskItems.splice(idx, 1) - // // } - // // } - // // } - - // // Repeater { - // // model: root.connectionsModel - - // // delegate: TaskConnection { - // // connection: connectionData - // // } - // // } - // } - - // // Flow info tooltip - // Rectangle { - // id: infoTooltip - // anchors.top: parent.bottom - // anchors.left: parent.left - // anchors.topMargin: 5 - // width: infoText.width + 20 - // height: infoText.height + 10 - // color: palette.base - // border.color: palette.shadow - // border.width: 1 - // radius: 4 - // visible: false - - // Text { - // id: infoText - // anchors.centerIn: parent - // text: "Tasks: " + (root.taskModel ? root.taskModel.rowCount() : 0) - // color: palette.text - // font.pixelSize: 10 - // } - // } - - // MouseArea { - // anchors.fill: parent - // hoverEnabled: true - - // onEntered: infoTooltip.visible = true - // onExited: infoTooltip.visible = false - // } - -} diff --git a/TaskFlow/Editor/qml/FlowEditorView.qml b/TaskFlow/Editor/qml/FlowEditorView.qml deleted file mode 100644 index 724b41d..0000000 --- a/TaskFlow/Editor/qml/FlowEditorView.qml +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -import QtQuick -import QtQuick.Controls -import TaskFlow.Editor - -FlowEditor { - id: root - - width: 1200 - height: 800 - - property SystemPalette sysPalette: SystemPalette { - colorGroup: SystemPalette.Active - } - palette { - window: sysPalette.window - windowText: sysPalette.windowText - base: sysPalette.base - alternateBase: sysPalette.alternateBase - text: sysPalette.text - button: sysPalette.button - buttonText: sysPalette.buttonText - highlight: sysPalette.highlight - highlightedText: sysPalette.highlightedText - light: sysPalette.light - mid: sysPalette.mid - dark: sysPalette.dark - shadow: sysPalette.shadow - brightText: sysPalette.brightText - } - - // Background with grid pattern - Rectangle { - anchors.fill: parent - color: palette.window - - // Grid pattern using C++ implementation - GridBackground { - anchors.fill: parent - gridSize: 20 - gridColor: palette.mid - opacity: 0.3 - } - } - - // Header panel - Rectangle { - id: headerPanel - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: 60 - color: palette.base - border.color: palette.mid - border.width: 1 - - Row { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 20 - spacing: 20 - - Text { - text: "Flow Editor" - color: palette.windowText - font.pixelSize: 18 - font.bold: true - } - - Rectangle { - width: 2 - height: 30 - color: palette.mid - } - - Text { - text: "Flow:" - color: palette.text - font.pixelSize: 14 - } - - ComboBox { - id: flowComboBox - - model: root.flowsModel - textRole: "flowId" - currentIndex: root.currentFlowIndex - - onActivated: { - root.currentFlowIndex = currentIndex - } - } - - Text { - text: "Available Tasks: " + root.availableTaskTypes.join(", ") - color: palette.text - font.pixelSize: 12 - } - } - } - - // Main flow area - ScrollView { - id: scrollView - anchors.top: headerPanel.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - - contentWidth: flow.width - contentHeight: flow.height - - Flow { - id: flow - - // flow: root.currentFlow - - width: Math.max(root.width, 0) - height: Math.min(root.height, 0) - } - } -} diff --git a/TaskFlow/Editor/qml/Task.qml b/TaskFlow/Editor/qml/Task.qml deleted file mode 100644 index de9f0f3..0000000 --- a/TaskFlow/Editor/qml/Task.qml +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -import QtQuick -import TaskFlow.Editor - -TaskItem{ - id: root - - width: 280 - height: Math.max(200, contentColumn.height + 40) - - DragHandler { - id: dragHandler - - target: root - onActiveChanged: { - if (active) { - root.z = 1000; // Поднять над остальными - } else { - root.z = 0; - } - } - } - - // Task node background - Rectangle { - anchors.fill: parent - color: palette.window - border.color: palette.shadow - border.width: 1 - radius: 6 - - // Task header - Rectangle { - id: taskHeader - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: 40 - color: palette.button - radius: 6 - - Rectangle { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - height: parent.radius - color: parent.color - } - - Text { - anchors.centerIn: parent - // text: root.taskType - color: palette.buttonText - font.pixelSize: 14 - font.bold: true - } - } - - // Task content - Column { - id: contentColumn - anchors.top: taskHeader.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 10 - spacing: 8 - - // Task ID - Text { - text: "ID: " + root.taskId - color: palette.text - font.pixelSize: 11 - width: parent.width - elide: Text.ElideRight - } - - // Parameters section - Item { - width: parent.width - height: paramColumn.height - // visible: root.parameters && root.parameters.rowCount() > 0 - - Column { - id: paramColumn - width: parent.width - spacing: 6 - - Text { - text: "Parameters:" - color: palette.text - font.pixelSize: 10 - font.bold: true - } - - Repeater { - model: root.parameters - delegate: Rectangle { - width: parent.width - height: 24 - color: palette.base - radius: 4 - - Row { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 8 - spacing: 6 - - Text { - text: paramKey + ":" - color: palette.text - font.pixelSize: 9 - font.bold: true - } - - Text { - text: paramValue - color: palette.windowText - font.pixelSize: 9 - width: Math.min(150, implicitWidth) - elide: Text.ElideRight - } - } - } - } - } - } - } - } - - // Input ports section (left side) - Column { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: -8 - spacing: 6 - // visible: root.inputPorts && root.inputPorts.rowCount() > 0 - - // Input label - Text { - text: "IN" - color: palette.highlight - font.pixelSize: 10 - font.bold: true - anchors.left: parent.left - anchors.leftMargin: -20 - } - - // Repeater { - // model: root.inputPorts - // delegate: Row { - // spacing: 6 - - // Text { - // text: taskPortName - // color: palette.text - // font.pixelSize: 9 - // anchors.verticalCenter: parent.verticalCenter - // horizontalAlignment: Text.AlignRight - // width: 60 - // elide: Text.ElideLeft - // } - - // TaskPort { - // port: taskPortData - // isInput: true - // } - // } - // } - } - - // Output ports section (right side) - Column { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: -10 - spacing: 8 - // visible: root.outputPorts && root.outputPorts.rowCount() > 0 - - // Output label - Text { - text: "OUT" - color: palette.highlight - font.pixelSize: 10 - font.bold: true - anchors.right: parent.right - anchors.rightMargin: -24 - } - - // Repeater { - // model: root.outputPorts - // delegate: Row { - // spacing: 6 - - // TaskPort { - // port: taskPortData - // isInput: false - // } - - // Text { - // text: taskPortName - // color: palette.text - // font.pixelSize: 9 - // anchors.verticalCenter: parent.verticalCenter - // width: 60 - // elide: Text.ElideRight - // } - // } - // } - } -} diff --git a/TaskFlow/Editor/qml/TaskConnection.qml b/TaskFlow/Editor/qml/TaskConnection.qml deleted file mode 100644 index abe4f09..0000000 --- a/TaskFlow/Editor/qml/TaskConnection.qml +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -import QtQuick -import QtQuick.Shapes -import TaskFlow.Editor - -TaskConnectionItem { - id: root - - property color connectionColor: "red" - - Rectangle { - width: 10 - height: 10 - radius: width / 2 - color: "blue" - } - - // width: Math.abs(endPoint.x - startPoint.x) + 40 - // height: Math.abs(endPoint.y - startPoint.y) + 40 - // x: Math.min(startPoint.x, endPoint.x) - 20 - // y: Math.min(startPoint.y, endPoint.y) - 20 - - // Shape { - // anchors.fill: parent - - // ShapePath { - // strokeWidth: 2 - // strokeColor: connectionColor - // fillColor: "transparent" - - // property point localStart: Qt.point( - // root.startPoint.x - root.x, - // root.startPoint.y - root.y - // ) - // property point localEnd: Qt.point( - // root.endPoint.x - root.x, - // root.endPoint.y - root.y - // ) - - // // Bezier curve - // property real controlOffset: Math.max(50, Math.abs(localEnd.x - localStart.x) * 0.4) - - // startX: localStart.x - // startY: localStart.y - - // PathCubic { - // x: parent.localEnd.x - // y: parent.localEnd.y - // control1X: parent.localStart.x + parent.controlOffset - // control1Y: parent.localStart.y - // control2X: parent.localEnd.x - parent.controlOffset - // control2Y: parent.localEnd.y - // } - // } - - // // Arrow head - // Rectangle { - // width: 8 - // height: 8 - // color: connectionColor - // rotation: 45 - // x: root.endPoint.x - root.x - 4 - // y: root.endPoint.y - root.y - 4 - // } - // } - - // // Update positions when tasks might have moved - // Component.onCompleted: updatePositions() -} diff --git a/TaskFlow/Editor/qml/TaskParameter.qml b/TaskFlow/Editor/qml/TaskParameter.qml deleted file mode 100644 index f1d179d..0000000 --- a/TaskFlow/Editor/qml/TaskParameter.qml +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -import QtQuick -import TaskFlow.Editor - -Item { - -} diff --git a/TaskFlow/Editor/qml/TaskPort.qml b/TaskFlow/Editor/qml/TaskPort.qml deleted file mode 100644 index b005ef1..0000000 --- a/TaskFlow/Editor/qml/TaskPort.qml +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -import QtQuick -import TaskFlow.Editor - -TaskPortItem { - id: root - - property bool isInput: true - - width: 20 - height: 20 - - // Port circle - Rectangle { - id: portCircle - anchors.centerIn: parent - width: 16 - height: 16 - radius: 8 - color: getPortColor() - border.color: palette.windowText - border.width: 1 - - // Inner circle for connected state simulation - Rectangle { - anchors.centerIn: parent - width: 8 - height: 8 - radius: 4 - color: root.port ? palette.windowText : "transparent" - visible: root.port !== null - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - - onEntered: { - portCircle.scale = 1.3 - portCircle.border.width = 2 - } - - onExited: { - portCircle.scale = 1.0 - portCircle.border.width = 1 - } - } - - function getPortColor() { - if (!root.port) return palette.mid - - // Different colors for input/output using system palette - if (root.isInput) { - return palette.highlight // System highlight color for inputs - } else { - return Qt.lighter(palette.highlight, 1.3) // Lighter highlight for outputs - } - } - - Behavior on scale { - NumberAnimation { duration: 100 } - } -} diff --git a/TaskFlow/core/BaseTask.cpp b/TaskFlow/core/BaseTask.cpp deleted file mode 100644 index 6e86a5f..0000000 --- a/TaskFlow/core/BaseTask.cpp +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "BaseTask.hpp" -#include "TaskPort.hpp" -#include -#include - -namespace QodeAssist::TaskFlow { - -BaseTask::BaseTask(QObject *parent) - : QObject(parent) - , m_taskId("unknown" + QUuid::createUuid().toString()) -{} - -BaseTask::~BaseTask() -{ - qDeleteAll(m_inputs); - qDeleteAll(m_outputs); -} - -QString BaseTask::taskId() const -{ - return m_taskId; -} - -void BaseTask::setTaskId(const QString &taskId) -{ - m_taskId = taskId; -} - -QString BaseTask::taskType() const -{ - return QString(metaObject()->className()).split("::").last(); -} - -void BaseTask::addInputPort(const QString &name) -{ - QMutexLocker locker(&m_tasksMutex); - m_inputs.append(new TaskPort(name, TaskPort::ValueType::Any, this)); -} - -void BaseTask::addOutputPort(const QString &name) -{ - QMutexLocker locker(&m_tasksMutex); - m_outputs.append(new TaskPort(name, TaskPort::ValueType::Any, this)); -} - -TaskPort *BaseTask::inputPort(const QString &name) const -{ - QMutexLocker locker(&m_tasksMutex); - - auto it = std::find_if(m_inputs.begin(), m_inputs.end(), [&name](const TaskPort *port) { - return port->name() == name; - }); - - return (it != m_inputs.end()) ? *it : nullptr; -} - -TaskPort *BaseTask::outputPort(const QString &name) const -{ - QMutexLocker locker(&m_tasksMutex); - - auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&name](const TaskPort *port) { - return port->name() == name; - }); - - return (it != m_outputs.end()) ? *it : nullptr; -} - -QList BaseTask::getInputPorts() const -{ - QMutexLocker locker(&m_tasksMutex); - return m_inputs; -} - -QList BaseTask::getOutputPorts() const -{ - QMutexLocker locker(&m_tasksMutex); - return m_outputs; -} - -QFuture BaseTask::executeAsync() -{ - return QtConcurrent::task([this]() -> TaskState { return execute(); }).spawn(); -} - -QString BaseTask::taskStateAsString(TaskState state) -{ - switch (state) { - case TaskState::Success: - return "Success"; - case TaskState::Failed: - return "Failed"; - case TaskState::Cancelled: - return "Cancelled"; - } - return "Unknown"; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/BaseTask.hpp b/TaskFlow/core/BaseTask.hpp deleted file mode 100644 index fd6b9d4..0000000 --- a/TaskFlow/core/BaseTask.hpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include - -namespace QodeAssist::TaskFlow { - -class TaskPort; - -enum class TaskState { Success, Failed, Cancelled }; - -class BaseTask : public QObject -{ - Q_OBJECT - -public: - explicit BaseTask(QObject *parent = nullptr); - virtual ~BaseTask(); - - QString taskId() const; - void setTaskId(const QString &taskId); - QString taskType() const; - - void addInputPort(const QString &name); - void addOutputPort(const QString &name); - - TaskPort *inputPort(const QString &name) const; - TaskPort *outputPort(const QString &name) const; - - QList getInputPorts() const; - QList getOutputPorts() const; - - virtual TaskState execute() = 0; - - static QString taskStateAsString(TaskState state); - -protected: - QFuture executeAsync(); - -private: - QString m_taskId; - QList m_inputs; - QList m_outputs; - mutable QMutex m_tasksMutex; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/CMakeLists.txt b/TaskFlow/core/CMakeLists.txt deleted file mode 100644 index a85b4db..0000000 --- a/TaskFlow/core/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -qt_add_library(TaskFlowCore STATIC - BaseTask.hpp BaseTask.cpp - TaskConnection.hpp TaskConnection.cpp - Flow.hpp Flow.cpp - TaskPort.hpp TaskPort.cpp - TaskRegistry.hpp TaskRegistry.cpp - FlowManager.hpp FlowManager.cpp - FlowRegistry.hpp FlowRegistry.cpp -) - -target_link_libraries(TaskFlowCore - PUBLIC - Qt::Core - Qt::Concurrent - PRIVATE - QodeAssistLogger -) - -target_include_directories(TaskFlowCore - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} -) diff --git a/TaskFlow/core/Flow.cpp b/TaskFlow/core/Flow.cpp deleted file mode 100644 index 4a4cfbf..0000000 --- a/TaskFlow/core/Flow.cpp +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "Flow.hpp" -#include "TaskPort.hpp" -#include -#include - -namespace QodeAssist::TaskFlow { - -Flow::Flow(QObject *parent) - : QObject(parent) - , m_flowId("flow_" + QUuid::createUuid().toString()) -{} - -Flow::~Flow() -{ - QMutexLocker locker(&m_flowMutex); - qDeleteAll(m_connections); - qDeleteAll(m_tasks); -} - -QString Flow::flowId() const -{ - return m_flowId; -} - -void Flow::setFlowId(const QString &flowId) -{ - if (m_flowId != flowId) { - m_flowId = flowId; - } -} - -void Flow::addTask(BaseTask *task) -{ - if (!task) { - return; - } - - QMutexLocker locker(&m_flowMutex); - - QString taskId = task->taskId(); - if (m_tasks.contains(taskId)) { - qWarning() << "Flow::addTask - Task with ID" << taskId << "already exists"; - return; - } - - m_tasks.insert(taskId, task); - task->setParent(this); - - emit taskAdded(taskId); -} - -void Flow::removeTask(const QString &taskId) -{ - QMutexLocker locker(&m_flowMutex); - - BaseTask *task = m_tasks.value(taskId); - if (!task) { - return; - } - - auto it = m_connections.begin(); - while (it != m_connections.end()) { - TaskConnection *connection = *it; - if (connection->sourceTask() == task || connection->targetTask() == task) { - it = m_connections.erase(it); - emit connectionRemoved(connection); - delete connection; - } else { - ++it; - } - } - - m_tasks.remove(taskId); - emit taskRemoved(taskId); - delete task; -} - -void Flow::removeTask(BaseTask *task) -{ - if (!task) { - return; - } - removeTask(task->taskId()); -} - -BaseTask *Flow::getTask(const QString &taskId) const -{ - QMutexLocker locker(&m_flowMutex); - return m_tasks.value(taskId); -} - -bool Flow::hasTask(const QString &taskId) const -{ - QMutexLocker locker(&m_flowMutex); - return m_tasks.contains(taskId); -} - -QHash Flow::tasks() const -{ - QMutexLocker locker(&m_flowMutex); - return m_tasks; -} - -TaskConnection *Flow::addConnection(TaskPort *sourcePort, TaskPort *targetPort) -{ - if (!sourcePort || !targetPort) { - qWarning() << "Flow::addConnection - Invalid ports"; - return nullptr; - } - - // Verify ports belong to tasks in this flow - BaseTask *sourceTask = qobject_cast(sourcePort->parent()); - BaseTask *targetTask = qobject_cast(targetPort->parent()); - - if (!sourceTask || !targetTask) { - qWarning() << "Flow::addConnection - Ports don't belong to valid tasks"; - return nullptr; - } - - QMutexLocker locker(&m_flowMutex); - - if (!m_tasks.contains(sourceTask->taskId()) || !m_tasks.contains(targetTask->taskId())) { - qWarning() << "Flow::addConnection - Tasks not in this flow"; - return nullptr; - } - - for (TaskConnection *existingConnection : m_connections) { - if (existingConnection->sourcePort() == sourcePort - && existingConnection->targetPort() == targetPort) { - qWarning() << "Flow::addConnection - Connection already exists"; - return existingConnection; - } - } - - TaskConnection *connection = new TaskConnection(sourcePort, targetPort, this); - m_connections.append(connection); - - emit connectionAdded(connection); - return connection; -} - -void Flow::removeConnection(TaskConnection *connection) -{ - if (!connection) { - return; - } - - QMutexLocker locker(&m_flowMutex); - - if (m_connections.removeOne(connection)) { - emit connectionRemoved(connection); - delete connection; - } -} - -QList Flow::connections() const -{ - QMutexLocker locker(&m_flowMutex); - return m_connections; -} - -QFuture Flow::executeAsync() -{ - return QtConcurrent::run([this]() { return execute(); }); -} - -FlowState Flow::execute() -{ - emit executionStarted(); - - if (!isValid()) { - emit executionFinished(FlowState::Failed); - return FlowState::Failed; - } - - if (hasCircularDependencies()) { - qWarning() << "Flow::execute - Circular dependencies detected"; - emit executionFinished(FlowState::Failed); - return FlowState::Failed; - } - - QList executionOrder = getExecutionOrder(); - - for (BaseTask *task : executionOrder) { - TaskState taskResult = task->execute(); - - if (taskResult == TaskState::Failed) { - qWarning() << "Flow::execute - Task" << task->taskId() << "failed"; - emit executionFinished(FlowState::Failed); - return FlowState::Failed; - } - - if (taskResult == TaskState::Cancelled) { - qWarning() << "Flow::execute - Task" << task->taskId() << "cancelled"; - emit executionFinished(FlowState::Cancelled); - return FlowState::Cancelled; - } - } - - emit executionFinished(FlowState::Success); - return FlowState::Success; -} - -bool Flow::isValid() const -{ - QMutexLocker locker(&m_flowMutex); - - // Check all connections are valid - for (TaskConnection *connection : m_connections) { - if (!connection->isValid()) { - return false; - } - } - - return true; -} - -bool Flow::hasCircularDependencies() const -{ - return detectCircularDependencies(); -} - -QString Flow::flowStateAsString(FlowState state) -{ - switch (state) { - case FlowState::Success: - return "Success"; - case FlowState::Failed: - return "Failed"; - case FlowState::Cancelled: - return "Cancelled"; - } - return "Unknown"; -} - -QStringList Flow::getTaskIds() const -{ - QMutexLocker locker(&m_flowMutex); - return m_tasks.keys(); -} - -QList Flow::getExecutionOrder() const -{ - QMutexLocker locker(&m_flowMutex); - - QList result; - QSet visited; - QList allTasks = m_tasks.values(); - - std::function visit = [&](BaseTask *task) { - if (visited.contains(task)) { - return; - } - - visited.insert(task); - - QList dependencies = getTaskDependencies(task); - for (BaseTask *dependency : dependencies) { - visit(dependency); - } - - result.append(task); - }; - - for (BaseTask *task : allTasks) { - visit(task); - } - - return result; -} - -bool Flow::detectCircularDependencies() const -{ - QMutexLocker locker(&m_flowMutex); - - QSet visited; - QSet recursionStack; - bool hasCycle = false; - - for (BaseTask *task : m_tasks.values()) { - if (!visited.contains(task)) { - visitTask(task, visited, recursionStack, hasCycle); - if (hasCycle) { - return true; - } - } - } - - return false; -} - -void Flow::visitTask( - BaseTask *task, QSet &visited, QSet &recursionStack, bool &hasCycle) const -{ - if (hasCycle) { - return; - } - - visited.insert(task); - recursionStack.insert(task); - - for (TaskConnection *connection : m_connections) { - if (connection->sourceTask() == task) { - BaseTask *dependentTask = connection->targetTask(); - - if (recursionStack.contains(dependentTask)) { - hasCycle = true; - return; - } - - if (!visited.contains(dependentTask)) { - visitTask(dependentTask, visited, recursionStack, hasCycle); - } - } - } - - recursionStack.remove(task); -} - -QList Flow::getTaskDependencies(BaseTask *task) const -{ - QList dependencies; - - for (TaskConnection *connection : m_connections) { - if (connection->targetTask() == task) { - BaseTask *dependencyTask = connection->sourceTask(); - if (!dependencies.contains(dependencyTask)) { - dependencies.append(dependencyTask); - } - } - } - - return dependencies; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/Flow.hpp b/TaskFlow/core/Flow.hpp deleted file mode 100644 index 74d305c..0000000 --- a/TaskFlow/core/Flow.hpp +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "BaseTask.hpp" -#include "TaskConnection.hpp" - -namespace QodeAssist::TaskFlow { - -enum class FlowState { Success, Failed, Cancelled }; - -class Flow : public QObject -{ - Q_OBJECT - -public: - explicit Flow(QObject *parent = nullptr); - ~Flow() override; - - QString flowId() const; - void setFlowId(const QString &flowId); - - void addTask(BaseTask *task); - void removeTask(const QString &taskId); - void removeTask(BaseTask *task); - - BaseTask *getTask(const QString &taskId) const; - bool hasTask(const QString &taskId) const; - QHash tasks() const; - - TaskConnection *addConnection(TaskPort *sourcePort, TaskPort *targetPort); - void removeConnection(TaskConnection *connection); - QList connections() const; - - QFuture executeAsync(); - virtual FlowState execute(); - - bool isValid() const; - bool hasCircularDependencies() const; - - static QString flowStateAsString(FlowState state); - QStringList getTaskIds() const; - -signals: - void taskAdded(const QString &taskId); - void taskRemoved(const QString &taskId); - void connectionAdded(QodeAssist::TaskFlow::TaskConnection *connection); - void connectionRemoved(QodeAssist::TaskFlow::TaskConnection *connection); - void executionStarted(); - void executionFinished(FlowState result); - -private: - QString m_flowId; - QHash m_tasks; - QList m_connections; - mutable QMutex m_flowMutex; - - QList getExecutionOrder() const; - bool detectCircularDependencies() const; - void visitTask( - BaseTask *task, - QSet &visited, - QSet &recursionStack, - bool &hasCycle) const; - QList getTaskDependencies(BaseTask *task) const; -}; - -} // namespace QodeAssist::TaskFlow - -Q_DECLARE_METATYPE(QodeAssist::TaskFlow::Flow *) -Q_DECLARE_METATYPE(QodeAssist::TaskFlow::FlowState) diff --git a/TaskFlow/core/FlowManager.cpp b/TaskFlow/core/FlowManager.cpp deleted file mode 100644 index ae5ac4c..0000000 --- a/TaskFlow/core/FlowManager.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "FlowManager.hpp" - -#include -#include -#include -#include - -#include "FlowRegistry.hpp" -#include "TaskRegistry.hpp" - -namespace QodeAssist::TaskFlow { - -FlowManager::FlowManager(QObject *parent) - : QObject(parent) - , m_taskRegistry(new TaskRegistry(this)) - , m_flowRegistry(new FlowRegistry(this)) -{ - LOG_MESSAGE("FlowManager created"); -} - -FlowManager::~FlowManager() -{ - clear(); -} - -// Flow *FlowManager::createFlow(const QString &flowId) -// { -// Flow *flow = new Flow(flowId, m_taskRegistry, this); -// if (!m_flows.contains(flow->flowId())) { -// m_flows.insert(flowId, flow); -// } else { -// LOG_MESSAGE( -// QString("FlowManager::createFlow - flow with id %1 already exists").arg(flow->flowId())); -// } - -// return flow; -// } - -void FlowManager::addFlow(Flow *flow) -{ - qDebug() << "FlowManager::addFlow" << flow->flowId(); - if (!m_flows.contains(flow->flowId())) { - m_flows.insert(flow->flowId(), flow); - flow->setParent(this); - emit flowAdded(flow->flowId()); - } else { - LOG_MESSAGE( - QString("FlowManager::addFlow - flow with id %1 already exists").arg(flow->flowId())); - } -} - -void FlowManager::clear() -{ - LOG_MESSAGE(QString("FlowManager::clear - removing %1 flows").arg(m_flows.size())); - - qDeleteAll(m_flows); - m_flows.clear(); -} - -QStringList FlowManager::getAvailableTasksTypes() -{ - return m_taskRegistry->getAvailableTypes(); -} - -QStringList FlowManager::getAvailableFlows() -{ - return m_flowRegistry->getAvailableTypes(); -} - -QHash FlowManager::flows() const -{ - return m_flows; -} - -TaskRegistry *FlowManager::taskRegistry() const -{ - return m_taskRegistry; -} - -FlowRegistry *FlowManager::flowRegistry() const -{ - return m_flowRegistry; -} - -Flow *FlowManager::getFlow(const QString &flowId) const -{ - // if (flowId.isEmpty()) { - // return m_flows.begin().value(); - // } - // return m_flows.value(flowId, nullptr); -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/FlowManager.hpp b/TaskFlow/core/FlowManager.hpp deleted file mode 100644 index 3aaec4b..0000000 --- a/TaskFlow/core/FlowManager.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include -#include - -#include "Flow.hpp" - -namespace QodeAssist::TaskFlow { - -class TaskRegistry; -class FlowRegistry; - -class FlowManager : public QObject -{ - Q_OBJECT - -public: - explicit FlowManager(QObject *parent = nullptr); - ~FlowManager() override; - - // Flow *createFlow(const QString &flowId); - void addFlow(Flow *flow); - - void clear(); - - QStringList getAvailableTasksTypes(); - QStringList getAvailableFlows(); - - QHash flows() const; - - TaskRegistry *taskRegistry() const; - FlowRegistry *flowRegistry() const; - - Flow *getFlow(const QString &flowId = {}) const; - -signals: - void flowAdded(const QString &flowId); - void flowRemoved(const QString &flowId); - -private: - QHash m_flows; - - TaskRegistry *m_taskRegistry; - FlowRegistry *m_flowRegistry; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/FlowRegistry.cpp b/TaskFlow/core/FlowRegistry.cpp deleted file mode 100644 index 97d27f5..0000000 --- a/TaskFlow/core/FlowRegistry.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "FlowRegistry.hpp" -#include "Logger.hpp" - -namespace QodeAssist::TaskFlow { - -FlowRegistry::FlowRegistry(QObject *parent) - : QObject(parent) -{} - -void FlowRegistry::registerFlow(const QString &flowType, FlowCreator creator) -{ - m_flowCreators[flowType] = creator; - LOG_MESSAGE(QString("FlowRegistry: Registered flow type '%1'").arg(flowType)); -} - -Flow *FlowRegistry::createFlow(const QString &flowType, FlowManager *flowManager) const -{ - LOG_MESSAGE(QString("Trying to create flow: %1").arg(flowType)); - - if (m_flowCreators.contains(flowType)) { - LOG_MESSAGE(QString("Found creator for flow type: %1").arg(flowType)); - try { - Flow *flow = m_flowCreators[flowType](flowManager); - if (flow) { - LOG_MESSAGE(QString("Successfully created flow: %1").arg(flowType)); - return flow; - } - } catch (...) { - LOG_MESSAGE(QString("Exception while creating flow of type: %1").arg(flowType)); - } - } else { - LOG_MESSAGE(QString("No creator found for flow type: %1").arg(flowType)); - } - - return nullptr; -} - -QStringList FlowRegistry::getAvailableTypes() const -{ - return m_flowCreators.keys(); -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/FlowRegistry.hpp b/TaskFlow/core/FlowRegistry.hpp deleted file mode 100644 index 2bb1ff3..0000000 --- a/TaskFlow/core/FlowRegistry.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include - -namespace QodeAssist::TaskFlow { - -class Flow; -class FlowManager; - -class FlowRegistry : public QObject -{ - Q_OBJECT -public: - using FlowCreator = std::function; - - explicit FlowRegistry(QObject *parent = nullptr); - - void registerFlow(const QString &flowType, FlowCreator creator); - Flow *createFlow(const QString &flowType, FlowManager *flowManager = nullptr) const; - QStringList getAvailableTypes() const; - -private: - QHash m_flowCreators; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/TaskConnection.cpp b/TaskFlow/core/TaskConnection.cpp deleted file mode 100644 index 155f1b8..0000000 --- a/TaskFlow/core/TaskConnection.cpp +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskConnection.hpp" -#include "BaseTask.hpp" -#include "TaskPort.hpp" -#include - -namespace QodeAssist::TaskFlow { - -TaskConnection::TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent) - : QObject(parent) - , m_sourcePort(sourcePort) - , m_targetPort(targetPort) -{ - setupConnection(); -} - -TaskConnection::~TaskConnection() -{ - cleanupConnection(); -} - -BaseTask *TaskConnection::sourceTask() const -{ - return m_sourcePort ? qobject_cast(m_sourcePort->parent()) : nullptr; -} - -BaseTask *TaskConnection::targetTask() const -{ - return m_targetPort ? qobject_cast(m_targetPort->parent()) : nullptr; -} - -TaskPort *TaskConnection::sourcePort() const -{ - return m_sourcePort; -} - -TaskPort *TaskConnection::targetPort() const -{ - return m_targetPort; -} - -bool TaskConnection::isValid() const -{ - return m_sourcePort && m_targetPort && m_sourcePort != m_targetPort && sourceTask() - && targetTask() && sourceTask() != targetTask(); -} - -bool TaskConnection::isTypeCompatible() const -{ - if (!isValid()) { - return false; - } - - return m_targetPort->isConnectionTypeCompatible(m_sourcePort); -} - -QString TaskConnection::toString() const -{ - if (!isValid()) { - return QString(); - } - - BaseTask *srcTask = sourceTask(); - BaseTask *tgtTask = targetTask(); - - return QString("%1.%2->%3.%4") - .arg(srcTask->taskId()) - .arg(m_sourcePort->name()) - .arg(tgtTask->taskId()) - .arg(m_targetPort->name()); -} - -bool TaskConnection::operator==(const TaskConnection &other) const -{ - return m_sourcePort == other.m_sourcePort && m_targetPort == other.m_targetPort; -} - -void TaskConnection::setupConnection() -{ - if (!isValid()) { - qWarning() << "TaskConnection::setupConnection - Invalid connection parameters"; - return; - } - - if (!isTypeCompatible()) { - QMetaEnum metaEnum = QMetaEnum::fromType(); - qWarning() << "TaskConnection::setupConnection - Type incompatible connection:" - << metaEnum.valueToKey(static_cast(m_sourcePort->valueType())) << "to" - << metaEnum.valueToKey(static_cast(m_targetPort->valueType())); - } - - m_sourcePort->setConnection(this); - m_targetPort->setConnection(this); -} - -void TaskConnection::cleanupConnection() -{ - if (m_sourcePort && m_sourcePort->connection() == this) { - m_sourcePort->setConnection(nullptr); - } - - if (m_targetPort && m_targetPort->connection() == this) { - m_targetPort->setConnection(nullptr); - } -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/TaskConnection.hpp b/TaskFlow/core/TaskConnection.hpp deleted file mode 100644 index 267409b..0000000 --- a/TaskFlow/core/TaskConnection.hpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::TaskFlow { - -class BaseTask; -class TaskPort; - -class TaskConnection : public QObject -{ - Q_OBJECT - -public: - // Constructor automatically sets up the connection - explicit TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent = nullptr); - - // Destructor automatically cleans up the connection - ~TaskConnection() override; - - // Getters - BaseTask *sourceTask() const; - BaseTask *targetTask() const; - TaskPort *sourcePort() const; - TaskPort *targetPort() const; - - // Validation - bool isValid() const; - bool isTypeCompatible() const; - - // Utility - QString toString() const; - - // Comparison - bool operator==(const TaskConnection &other) const; - -private: - TaskPort *m_sourcePort; - TaskPort *m_targetPort; - - void setupConnection(); - void cleanupConnection(); -}; - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/TaskPort.cpp b/TaskFlow/core/TaskPort.cpp deleted file mode 100644 index bc0964c..0000000 --- a/TaskFlow/core/TaskPort.cpp +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskPort.hpp" -#include "TaskConnection.hpp" -#include - -namespace QodeAssist::TaskFlow { - -TaskPort::TaskPort(const QString &name, ValueType type, QObject *parent) - : QObject(parent) - , m_name(name) - , m_valueType(type) -{} - -QString TaskPort::name() const -{ - return m_name; -} - -void TaskPort::setValueType(ValueType type) -{ - if (m_valueType != type) - m_valueType = type; -} - -TaskPort::ValueType TaskPort::valueType() const -{ - return m_valueType; -} - -void TaskPort::setValue(const QVariant &value) -{ - if (!isValueTypeCompatible(value)) { - qWarning() << "TaskPort::setValue - Type mismatch for port" << m_name << "Expected:" - << QMetaEnum::fromType().valueToKey(static_cast(m_valueType)) - << "Got:" << value.typeName(); - } - - if (m_value != value) { - m_value = value; - emit valueChanged(); - } -} - -QVariant TaskPort::value() const -{ - if (hasConnection() && m_connection->sourcePort()) { - return m_connection->sourcePort()->m_value; - } - return m_value; -} - -void TaskPort::setConnection(TaskConnection *connection) -{ - if (m_connection != connection) { - m_connection = connection; - emit connectionChanged(); - } -} - -TaskConnection *TaskPort::connection() const -{ - return m_connection; -} - -bool TaskPort::hasConnection() const -{ - return m_connection != nullptr; -} - -bool TaskPort::isValueTypeCompatible(const QVariant &value) const -{ - if (m_valueType == ValueType::Any) { - return true; - } - - switch (m_valueType) { - case ValueType::String: - return value.canConvert(); - - case ValueType::Number: - return value.canConvert() || value.canConvert(); - - case ValueType::Boolean: - return value.canConvert(); - - default: - return false; - } -} - -bool TaskPort::isConnectionTypeCompatible(const TaskPort *sourcePort) const -{ - if (!sourcePort) { - return false; - } - - if (sourcePort->valueType() == ValueType::Any || m_valueType == ValueType::Any) { - return true; - } - - return sourcePort->valueType() == m_valueType; -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/TaskPort.hpp b/TaskFlow/core/TaskPort.hpp deleted file mode 100644 index 215c98a..0000000 --- a/TaskFlow/core/TaskPort.hpp +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include - -#include "TaskConnection.hpp" - -namespace QodeAssist::TaskFlow { - -class TaskPort : public QObject -{ - Q_OBJECT - -public: - enum class ValueType { - Any, // QVariant - String, // QString - Number, // int/double - Boolean // bool - }; - Q_ENUM(ValueType) - - explicit TaskPort( - const QString &name, ValueType type = ValueType::Any, QObject *parent = nullptr); - - QString name() const; - - ValueType valueType() const; - void setValueType(ValueType type); - - void setValue(const QVariant &value); - QVariant value() const; - - void setConnection(TaskConnection *connection); - TaskConnection *connection() const; - bool hasConnection() const; - - bool isValueTypeCompatible(const QVariant &value) const; - bool isConnectionTypeCompatible(const TaskPort *sourcePort) const; - -signals: - void valueChanged(); - void connectionChanged(); - -private: - QString m_name; - ValueType m_valueType; - QVariant m_value; - TaskConnection *m_connection = nullptr; -}; - -} // namespace QodeAssist::TaskFlow - -Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort *) -Q_DECLARE_METATYPE(QodeAssist::TaskFlow::TaskPort::ValueType) diff --git a/TaskFlow/core/TaskRegistry.cpp b/TaskFlow/core/TaskRegistry.cpp deleted file mode 100644 index 70be3c8..0000000 --- a/TaskFlow/core/TaskRegistry.cpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TaskRegistry.hpp" - -#include - -#include "BaseTask.hpp" - -namespace QodeAssist::TaskFlow { - -TaskRegistry::TaskRegistry(QObject *parent) - : QObject(parent) -{} - -BaseTask *TaskRegistry::createTask(const QString &taskType, QObject *parent) const -{ - LOG_MESSAGE(QString("Trying to create task: %1").arg(taskType)); - - if (m_creators.contains(taskType)) { - LOG_MESSAGE(QString("Found creator for task type: %1").arg(taskType)); - try { - BaseTask *task = m_creators[taskType](parent); - if (task) { - LOG_MESSAGE(QString("Successfully created task: %1").arg(taskType)); - return task; - } - } catch (...) { - LOG_MESSAGE(QString("Exception while creating task of type: %1").arg(taskType)); - } - } else { - LOG_MESSAGE(QString("No creator found for task type: %1").arg(taskType)); - } - - return nullptr; -} - -QStringList TaskRegistry::getAvailableTypes() const -{ - return m_creators.keys(); -} - -} // namespace QodeAssist::TaskFlow diff --git a/TaskFlow/core/TaskRegistry.hpp b/TaskFlow/core/TaskRegistry.hpp deleted file mode 100644 index eec0df0..0000000 --- a/TaskFlow/core/TaskRegistry.hpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include - -namespace QodeAssist::TaskFlow { - -class BaseTask; - -class TaskRegistry : public QObject -{ - Q_OBJECT -public: - using TaskCreator = std::function; - - explicit TaskRegistry(QObject *parent = nullptr); - - template - inline void registerTask(const QString &taskType) - { - m_creators[taskType] = [](QObject *parent) -> BaseTask * { return new T(parent); }; - } - BaseTask *createTask(const QString &taskType, QObject *parent = nullptr) const; - QStringList getAvailableTypes() const; - -private: - QHash m_creators; -}; - -} // namespace QodeAssist::TaskFlow diff --git a/qodeassist.cpp b/qodeassist.cpp index c678a7e..1964f7e 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -54,21 +54,9 @@ #include "settings/ChatAssistantSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProjectSettingsPanel.hpp" -#ifdef QODEASSIST_EXPERIMENTAL -#include "settings/AgentsSettingsPage.hpp" -#include "settings/ProvidersSettingsPage.hpp" -#include "sources/settings/AgentPipelinesPage.hpp" -#endif #include "settings/QuickRefactorSettings.hpp" #include "settings/SettingsConstants.hpp" -#ifdef QODEASSIST_EXPERIMENTAL -#include "ProviderInstanceFactory.hpp" -#include "ProviderLauncher.hpp" -#include "ProviderSecretsStore.hpp" - -#include -#endif #include "templates/Templates.hpp" #include "widgets/CustomInstructionsManager.hpp" #include "widgets/QuickRefactorDialog.hpp" @@ -209,27 +197,6 @@ public: Settings::setupProjectPanel(); ConfigurationManager::instance().init(); -#ifdef QODEASSIST_EXPERIMENTAL - m_providerInstanceFactory = new Providers::ProviderInstanceFactory(this); - m_providerSecretsStore = new Providers::ProviderSecretsStore(this); - m_providerLauncher = new Providers::ProviderLauncher(this); - m_providersPageNavigator = new Settings::ProvidersPageNavigator(this); - m_providersOptionsPage = Settings::createProvidersSettingsPage( - m_providerInstanceFactory, - m_providerSecretsStore, - m_providerLauncher, - m_providersPageNavigator); - - m_agentFactory = new AgentFactory(m_providerInstanceFactory, m_providerSecretsStore, this); - m_agentsPageNavigator = new Settings::AgentsPageNavigator(this); - m_agentsOptionsPage = Settings::createAgentsSettingsPage( - m_agentFactory, m_agentsPageNavigator); - - m_agentPipelinesPageNavigator = new Settings::AgentPipelinesPageNavigator(this); - m_agentPipelinesOptionsPage = Settings::createAgentPipelinesSettingsPage( - m_agentFactory, m_agentPipelinesPageNavigator, m_agentsPageNavigator); -#endif - m_mcpServerManager = new Mcp::McpServerManager(this); m_mcpServerManager->init(); @@ -524,18 +491,6 @@ private: QPointer m_mcpServerManager; QPointer m_engine; QPointer m_skillsManager; -#ifdef QODEASSIST_EXPERIMENTAL - QPointer m_providerInstanceFactory; - QPointer m_providerSecretsStore; - QPointer m_providerLauncher; - QPointer m_providersPageNavigator; - std::unique_ptr m_providersOptionsPage; - QPointer m_agentFactory; - QPointer m_agentsPageNavigator; - std::unique_ptr m_agentsOptionsPage; - QPointer m_agentPipelinesPageNavigator; - std::unique_ptr m_agentPipelinesOptionsPage; -#endif }; } // namespace QodeAssist::Internal diff --git a/settings/AgentDetailPane.cpp b/settings/AgentDetailPane.cpp deleted file mode 100644 index dba1b20..0000000 --- a/settings/AgentDetailPane.cpp +++ /dev/null @@ -1,476 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentDetailPane.hpp" - -#include "SectionBox.hpp" -#include "SettingsTheme.hpp" -#include "SettingsUiBuilders.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -namespace { - -constexpr qint64 kRawTomlMaxBytes = 256 * 1024; - -enum class FileReadStatus { Ok, Empty, Truncated, OpenFailed }; - -struct FileReadResult -{ - FileReadStatus status = FileReadStatus::OpenFailed; - QString content; - QString error; -}; - -FileReadResult readFileTextCapped(const QString &path, qint64 maxBytes) -{ - FileReadResult result; - QFile f(path); - if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { - result.status = FileReadStatus::OpenFailed; - result.error = f.errorString(); - return result; - } - const qint64 size = f.size(); - const QByteArray bytes = f.read(maxBytes); - result.content = QString::fromUtf8(bytes); - if (size == 0) - result.status = FileReadStatus::Empty; - else if (size > maxBytes) - result.status = FileReadStatus::Truncated; - else - result.status = FileReadStatus::Ok; - return result; -} - -} // namespace - -AgentDetailPane::AgentDetailPane(QWidget *parent) - : QWidget(parent) -{ - m_name = new QLabel(this); - QFont nf = m_name->font(); - nf.setBold(true); - nf.setPixelSize(15); - m_name->setFont(nf); - m_name->setTextInteractionFlags(Qt::TextSelectableByMouse); - - m_path = new QLabel(this); - m_path->setFont(monospaceFont(11)); - m_path->setTextInteractionFlags(Qt::TextSelectableByMouse); - QPalette pp = m_path->palette(); - pp.setColor(QPalette::WindowText, pp.color(QPalette::Mid)); - m_path->setPalette(pp); - - m_openBtn = new QPushButton(tr("Open in editor"), this); - m_dupBtn = new QPushButton(tr("Duplicate…"), this); - m_deleteBtn = new QPushButton(tr("Delete"), this); - connect(m_openBtn, &QPushButton::clicked, this, - [this] { if (m_current) emit openInEditorRequested(*m_current); }); - connect(m_dupBtn, &QPushButton::clicked, this, - [this] { if (m_current) emit customizeRequested(*m_current); }); - connect(m_deleteBtn, &QPushButton::clicked, this, - [this] { if (m_current) emit deleteRequested(*m_current); }); - - auto *actions = new QHBoxLayout; - actions->setContentsMargins(0, 0, 0, 0); - actions->setSpacing(6); - actions->addWidget(m_openBtn); - actions->addWidget(m_dupBtn); - actions->addWidget(m_deleteBtn); - - auto *titleRow = new QHBoxLayout; - titleRow->setContentsMargins(0, 0, 0, 0); - titleRow->setSpacing(8); - titleRow->addWidget(m_name); - titleRow->addStretch(1); - - auto *headerLeft = new QVBoxLayout; - headerLeft->setContentsMargins(0, 0, 0, 0); - headerLeft->setSpacing(2); - headerLeft->addLayout(titleRow); - headerLeft->addWidget(m_path); - - auto *headerRow = new QHBoxLayout; - headerRow->setContentsMargins(0, 0, 0, 0); - headerRow->setSpacing(8); - headerRow->addLayout(headerLeft, 1); - headerRow->addLayout(actions); - - auto *headerSep = new QFrame(this); - headerSep->setFrameShape(QFrame::HLine); - headerSep->setFrameShadow(QFrame::Sunken); - - m_description = new QLabel(this); - m_description->setWordWrap(true); - m_description->setTextInteractionFlags(Qt::TextSelectableByMouse); - - auto *identity = new SectionBox(tr("Identity"), this); - m_nameValue = makeReadOnlyLine(); - m_extendsLabel = new QLabel(tr("Extends:"), this); - m_extendsLabel->setMinimumWidth(96); - m_extendsLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); - m_extendsValue = makeReadOnlyLine(); - m_descriptionEdit = new QPlainTextEdit(this); - m_descriptionEdit->setReadOnly(true); - m_descriptionEdit->setMaximumHeight(56); - m_tagsValue = makeReadOnlyLine(); - - auto *idGrid = new QGridLayout; - idGrid->setContentsMargins(0, 0, 0, 0); - idGrid->setHorizontalSpacing(8); - idGrid->setVerticalSpacing(4); - FormBuilder idForm(idGrid); - idForm.row(tr("Name:"), m_nameValue); - { - auto *holder = new QWidget; - holder->setLayout(singleField(m_extendsValue)); - const int row = idForm.currentRow(); - idGrid->addWidget(m_extendsLabel, row, 0, Qt::AlignTop); - idGrid->addWidget(holder, row, 1); - m_extendsHolder = holder; - idForm = FormBuilder(idGrid, row + 1); - } - idForm.row(tr("Description:"), m_descriptionEdit); - idForm.row(tr("Tags:"), m_tagsValue, - tr("Comma-separated. Free-form — used to filter and " - "group the agent list.")); - identity->bodyLayout()->addLayout(idGrid); - - auto *roleSection = new SectionBox(tr("System role"), this); - auto *roleHint = makeHintLabel( - tr("Prepended to every request as the system message.")); - m_roleText = new QPlainTextEdit(this); - m_roleText->setReadOnly(true); - m_roleText->setMinimumHeight(120); - roleSection->bodyLayout()->addWidget(roleHint); - roleSection->bodyLayout()->addWidget(m_roleText); - - auto *contextSection = new SectionBox(tr("Context"), this); - auto *contextHint = makeHintLabel( - tr("Jinja2 template rendered with ContextManager bindings into the " - "agent.context system-prompt layer. Empty = no context block.")); - m_contextText = new QPlainTextEdit(this); - m_contextText->setReadOnly(true); - m_contextText->setFont(monospaceFont(11)); - m_contextText->setMinimumHeight(120); - contextSection->bodyLayout()->addWidget(contextHint); - contextSection->bodyLayout()->addWidget(m_contextText); - - auto *connection = new SectionBox(tr("Connection"), this); - m_providerCombo = new QComboBox(this); - m_providerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents); - m_providerCombo->setEnabled(false); - m_endpointValue = makeReadOnlyLine(true); - m_modelValue = makeReadOnlyLine(true); - - auto *connGrid = new QGridLayout; - connGrid->setContentsMargins(0, 0, 0, 0); - connGrid->setHorizontalSpacing(8); - connGrid->setVerticalSpacing(4); - FormBuilder(connGrid) - .row(tr("Provider:"), m_providerCombo, - tr("The provider instance this agent uses. URL is " - "inherited from the instance.")) - .row(tr("Endpoint:"), m_endpointValue, - tr("Appended to the provider's URL. Blank uses the " - "provider default.")) - .row(tr("Model:"), m_modelValue); - connection->bodyLayout()->addLayout(connGrid); - - m_effectiveUrl = new QLabel(this); - m_effectiveUrl->setFont(monospaceFont(11)); - m_effectiveUrl->setTextInteractionFlags(Qt::TextSelectableByMouse); - m_effectiveUrl->setWordWrap(true); - m_effectiveUrl->setContentsMargins(6, 4, 6, 4); - m_effectiveUrl->setAutoFillBackground(true); - connection->bodyLayout()->addWidget(m_effectiveUrl); - - auto *match = new SectionBox(tr("Match"), this); - auto *matchHint = makeHintLabel( - tr("When a feature slot has multiple bound agents, the first whose " - "match rules satisfy the current context wins.")); - m_filePatternsValue = makeReadOnlyLine(true); - auto *matchGrid = new QGridLayout; - matchGrid->setContentsMargins(0, 0, 0, 0); - matchGrid->setHorizontalSpacing(8); - matchGrid->setVerticalSpacing(4); - FormBuilder(matchGrid).row(tr("File patterns:"), m_filePatternsValue, - tr("Globs, comma-separated. Empty matches every file.")); - match->bodyLayout()->addWidget(matchHint); - match->bodyLayout()->addLayout(matchGrid); - - auto *templ = new SectionBox(tr("Template"), this); - auto *templHint = makeHintLabel( - tr("Jinja2 template (via inja) rendered to the request body. " - "Built-in context: ctx.prefix, ctx.suffix, ctx.history, " - "ctx.system_prompt, agent.model.")); - m_messageFormat = new QPlainTextEdit(this); - m_messageFormat->setReadOnly(true); - m_messageFormat->setFont(monospaceFont(11)); - m_messageFormat->setMinimumHeight(140); - - templ->bodyLayout()->addWidget(templHint); - auto *mfLabel = new QLabel(tr("message_format:"), this); - templ->bodyLayout()->addWidget(mfLabel); - templ->bodyLayout()->addWidget(m_messageFormat); - - m_diagnostics = new SectionBox(tr("Load errors"), this); - m_diagnosticsView = new QPlainTextEdit(this); - m_diagnosticsView->setReadOnly(true); - m_diagnosticsView->setMaximumHeight(110); - m_diagnosticsView->setFont(monospaceFont(11)); - m_diagnostics->bodyLayout()->addWidget(m_diagnosticsView); - m_diagnostics->setVisible(false); - - m_rawToggle = new QToolButton(this); - m_rawToggle->setText(tr("▸ Show raw TOML")); - m_rawToggle->setCursor(Qt::PointingHandCursor); - m_rawToggle->setAutoRaise(true); - m_rawToggle->setCheckable(true); - m_rawToml = new QPlainTextEdit(this); - m_rawToml->setReadOnly(true); - m_rawToml->setFont(monospaceFont(11)); - m_rawToml->setMinimumHeight(140); - m_rawToml->setVisible(false); - connect(m_rawToggle, &QToolButton::toggled, this, [this](bool on) { - m_rawToml->setVisible(on); - m_rawToggle->setText(on ? tr("▾ Hide raw TOML") : tr("▸ Show raw TOML")); - }); - - auto *root = new QVBoxLayout(this); - root->setContentsMargins(12, 12, 12, 12); - root->setSpacing(10); - root->addLayout(headerRow); - root->addWidget(headerSep); - root->addWidget(m_description); - root->addWidget(identity); - root->addWidget(connection); - root->addWidget(match); - root->addWidget(templ); - root->addWidget(roleSection); - root->addWidget(contextSection); - root->addWidget(m_diagnostics); - root->addWidget(m_rawToggle, 0, Qt::AlignLeft); - root->addWidget(m_rawToml); - root->addStretch(1); - - clear(); - applyCodePalette(); -} - -void AgentDetailPane::setInstanceFactory(Providers::ProviderInstanceFactory *factory) -{ - m_instanceFactory = factory; - m_providerComboPopulated = false; - populateProviderCombo(); -} - -void AgentDetailPane::populateProviderCombo() -{ - if (m_providerComboPopulated) - return; - m_providerCombo->clear(); - m_providerComboHasSentinel = false; - if (m_instanceFactory) { - for (const auto &inst : m_instanceFactory->instances()) { - m_providerCombo->addItem( - QStringLiteral("%1 (%2)").arg(inst.name, inst.clientApi), inst.name); - } - } - m_providerComboPopulated = true; -} - -void AgentDetailPane::setAgent(const AgentConfig &cfg) -{ - m_currentStorage = cfg; - m_current = &m_currentStorage; - const bool user = cfg.isUserSource(); - - m_name->setText(cfg.name); - m_path->setText(cfg.sourcePath); - m_description->setText(cfg.description.isEmpty() - ? tr("No description provided.") - : cfg.description); - - m_nameValue->setText(cfg.name); - if (cfg.extendsName.isEmpty()) { - m_extendsLabel->setVisible(false); - m_extendsHolder->setVisible(false); - } else { - m_extendsLabel->setVisible(true); - m_extendsHolder->setVisible(true); - m_extendsValue->setText(cfg.extendsName); - } - m_descriptionEdit->setPlainText(cfg.description); - m_tagsValue->setText(cfg.tags.join(QStringLiteral(", "))); - - populateProviderCombo(); - - if (m_providerComboHasSentinel) { - m_providerCombo->removeItem(0); - m_providerComboHasSentinel = false; - } - - QString resolvedUrl; - if (m_instanceFactory) { - if (const auto *inst = m_instanceFactory->instanceByName(cfg.providerInstance)) - resolvedUrl = inst->url; - } - const int idx = m_providerCombo->findData(cfg.providerInstance); - if (idx >= 0) { - m_providerCombo->setCurrentIndex(idx); - } else if (!cfg.providerInstance.isEmpty()) { - m_providerCombo->insertItem( - 0, tr("%1 (missing — not in provider library)") - .arg(cfg.providerInstance), - cfg.providerInstance); - m_providerCombo->setCurrentIndex(0); - m_providerComboHasSentinel = true; - } - - m_endpointValue->setText(cfg.endpoint); - m_endpointValue->setPlaceholderText(tr("(provider default)")); - m_modelValue->setText(cfg.model); - - const QString eff = resolvedUrl + cfg.endpoint; - m_effectiveUrl->setText( - eff.isEmpty() - ? tr("# effective request line\n(unknown — provider instance not found)") - : QStringLiteral("# %1\nPOST %2") - .arg(tr("effective request line"), eff)); - - m_roleText->setPlainText( - cfg.role.isEmpty() ? tr("(no system role set)") : cfg.role); - m_contextText->setPlainText( - cfg.context.isEmpty() ? tr("(no context block)") : cfg.context); - - m_filePatternsValue->setText(cfg.match.filePatterns.join(QStringLiteral(", "))); - m_filePatternsValue->setPlaceholderText(tr("(matches every file)")); - - m_messageFormat->setPlainText( - cfg.messageFormat.isEmpty() ? tr("(inherited from parent / none)") - : cfg.messageFormat); - - const FileReadResult raw = readFileTextCapped(cfg.sourcePath, kRawTomlMaxBytes); - switch (raw.status) { - case FileReadStatus::Ok: - m_rawToml->setPlainText(raw.content); - break; - case FileReadStatus::Truncated: - m_rawToml->setPlainText( - raw.content + QStringLiteral("\n\n") - + tr("(truncated at %1 bytes)").arg(kRawTomlMaxBytes)); - break; - case FileReadStatus::Empty: - m_rawToml->setPlainText(tr("(source file is empty)")); - break; - case FileReadStatus::OpenFailed: - m_rawToml->setPlainText(tr("(source file unavailable: %1)").arg(raw.error)); - break; - } - - m_openBtn->setEnabled(user); - m_openBtn->setToolTip(user ? QString() - : tr("Bundled agents are read-only — " - "duplicate to edit.")); - m_deleteBtn->setEnabled(user); - m_deleteBtn->setToolTip(user ? QString() - : tr("Bundled agents cannot be deleted.")); - m_dupBtn->setEnabled(true); - applyCodePalette(); -} - -void AgentDetailPane::clear() -{ - m_currentStorage = AgentConfig{}; - m_current = nullptr; - m_name->setText(tr("Select an agent")); - m_path->clear(); - m_description->setText(tr("Pick an agent from the list to see its details.")); - m_nameValue->clear(); - m_extendsLabel->setVisible(false); - m_extendsHolder->setVisible(false); - m_descriptionEdit->clear(); - m_tagsValue->clear(); - if (m_providerComboHasSentinel) { - m_providerCombo->removeItem(0); - m_providerComboHasSentinel = false; - } - m_providerCombo->setCurrentIndex(-1); - m_endpointValue->clear(); - m_modelValue->clear(); - m_effectiveUrl->clear(); - m_roleText->clear(); - m_contextText->clear(); - m_filePatternsValue->clear(); - m_messageFormat->clear(); - m_rawToml->clear(); - m_openBtn->setEnabled(false); - m_dupBtn->setEnabled(false); - m_deleteBtn->setEnabled(false); -} - -void AgentDetailPane::setLoadDiagnostics(const QStringList &errors, const QStringList &warnings) -{ - QStringList lines; - for (const QString &e : errors) - lines << tr("error: %1").arg(e); - for (const QString &w : warnings) - lines << tr("warning: %1").arg(w); - m_diagnostics->setVisible(!lines.isEmpty()); - m_diagnosticsView->setPlainText(lines.join(QLatin1Char('\n'))); -} - -void AgentDetailPane::changeEvent(QEvent *event) -{ - QWidget::changeEvent(event); - if (m_inApplyPalette) - return; - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) - applyCodePalette(); -} - -QLineEdit *AgentDetailPane::makeReadOnlyLine(bool mono) -{ - auto *e = new QLineEdit(this); - e->setReadOnly(true); - if (mono) - e->setFont(monospaceFont(11)); - return e; -} - -void AgentDetailPane::applyCodePalette() -{ - QScopedValueRollback guard(m_inApplyPalette, true); - const Theme theme = themeFor(palette()); - QPalette p = m_effectiveUrl->palette(); - p.setColor(QPalette::Window, QColor(theme.codeBg)); - p.setColor(QPalette::WindowText, palette().color(QPalette::Text)); - m_effectiveUrl->setPalette(p); - m_effectiveUrl->setStyleSheet(QStringLiteral( - "QLabel { background:%1; border:1px solid %2; }") - .arg(theme.codeBg, theme.rowSeparator)); -} - -} // namespace QodeAssist::Settings diff --git a/settings/AgentDetailPane.hpp b/settings/AgentDetailPane.hpp deleted file mode 100644 index cac0bfe..0000000 --- a/settings/AgentDetailPane.hpp +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include - -#include - -class QComboBox; -class QLabel; -class QLineEdit; -class QPlainTextEdit; -class QPushButton; -class QToolButton; - -namespace QodeAssist::Providers { -class ProviderInstanceFactory; -} - -namespace QodeAssist::Settings { - -class SectionBox; - -class AgentDetailPane : public QWidget -{ - Q_OBJECT -public: - explicit AgentDetailPane(QWidget *parent = nullptr); - - void setInstanceFactory(Providers::ProviderInstanceFactory *factory); - void setAgent(const AgentConfig &cfg); - void clear(); - void setLoadDiagnostics(const QStringList &errors, const QStringList &warnings); - -signals: - void openInEditorRequested(const AgentConfig &cfg); - void customizeRequested(const AgentConfig &cfg); - void deleteRequested(const AgentConfig &cfg); - -protected: - void changeEvent(QEvent *event) override; - -private: - QLineEdit *makeReadOnlyLine(bool mono = false); - void applyCodePalette(); - void populateProviderCombo(); - - bool m_inApplyPalette = false; - bool m_providerComboPopulated = false; - bool m_providerComboHasSentinel = false; - - AgentConfig m_currentStorage; - const AgentConfig *m_current = nullptr; - - QLabel *m_name = nullptr; - QLabel *m_path = nullptr; - QPushButton *m_openBtn = nullptr; - QPushButton *m_dupBtn = nullptr; - QPushButton *m_deleteBtn = nullptr; - QLabel *m_description = nullptr; - - QLineEdit *m_nameValue = nullptr; - QLabel *m_extendsLabel = nullptr; - QWidget *m_extendsHolder = nullptr; - QLineEdit *m_extendsValue = nullptr; - QPlainTextEdit *m_descriptionEdit = nullptr; - QLineEdit *m_tagsValue = nullptr; - - QComboBox *m_providerCombo = nullptr; - QPointer m_instanceFactory; - QLineEdit *m_endpointValue = nullptr; - QLineEdit *m_modelValue = nullptr; - QLabel *m_effectiveUrl = nullptr; - - QLineEdit *m_filePatternsValue = nullptr; - - QPlainTextEdit *m_roleText = nullptr; - QPlainTextEdit *m_contextText = nullptr; - QPlainTextEdit *m_messageFormat = nullptr; - - SectionBox *m_diagnostics = nullptr; - QPlainTextEdit *m_diagnosticsView = nullptr; - - QToolButton *m_rawToggle = nullptr; - QPlainTextEdit *m_rawToml = nullptr; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/AgentDuplicator.cpp b/settings/AgentDuplicator.cpp deleted file mode 100644 index 6ad4868..0000000 --- a/settings/AgentDuplicator.cpp +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentDuplicator.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -namespace { - -QString tomlEscape(const QString &s) -{ - QString out; - out.reserve(s.size()); - for (QChar c : s) { - switch (c.unicode()) { - case '\\': out += QLatin1String("\\\\"); break; - case '"': out += QLatin1String("\\\""); break; - case '\n': out += QLatin1String("\\n"); break; - case '\r': out += QLatin1String("\\r"); break; - case '\t': out += QLatin1String("\\t"); break; - default: out += c; - } - } - return out; -} - -constexpr int kMaxUniqueAttempts = 1000; - -QString uniqueFilename(const QString &userDir, const QString &parentBasename) -{ - QString fileName = parentBasename + QStringLiteral("_custom.toml"); - for (int n = 2; n < kMaxUniqueAttempts - && QFile::exists(QDir(userDir).filePath(fileName)); - ++n) - fileName = QStringLiteral("%1_custom_%2.toml").arg(parentBasename).arg(n); - return QDir(userDir).filePath(fileName); -} - -QString uniqueName(const QString &parentName, const AgentFactory &factory) -{ - QString newName = QStringLiteral("%1 (Custom)").arg(parentName); - for (int n = 2; n < kMaxUniqueAttempts && factory.configByName(newName); ++n) - newName = QStringLiteral("%1 (Custom %2)").arg(parentName).arg(n); - return newName; -} - -QString trUser(const char *src) -{ - return QCoreApplication::translate("QodeAssist::Settings::AgentDuplicator", src); -} - -} // namespace - -AgentDuplicateResult duplicateAgentInUserDir( - const AgentConfig &parent, const AgentFactory &factory) -{ - AgentDuplicateResult result; - if (parent.name.trimmed().isEmpty()) { - result.error = trUser("Parent agent has no name; cannot duplicate."); - return result; - } - - const QString userDir = AgentFactory::userAgentsDir(); - if (!QDir().mkpath(userDir)) { - result.error = trUser("Cannot create user agents folder: %1").arg(userDir); - return result; - } - - const QString parentBasename = QFileInfo(parent.sourcePath).baseName(); - result.filePath = uniqueFilename(userDir, parentBasename); - if (QFile::exists(result.filePath)) { - result.error = trUser("Could not find a free filename after %1 attempts.") - .arg(kMaxUniqueAttempts); - return result; - } - result.newName = uniqueName(parent.name, factory); - if (factory.configByName(result.newName)) { - result.error = trUser("Could not find a free agent name after %1 attempts.") - .arg(kMaxUniqueAttempts); - return result; - } - - QSaveFile f(result.filePath); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - result.error = trUser("Cannot create %1: %2").arg(result.filePath, f.errorString()); - return result; - } - const QString description - = QStringLiteral("User customization of '%1'. Override fields below to taste; " - "values not overridden are inherited from the parent.") - .arg(parent.name); - const QString body = QStringLiteral( - "schema_version = 1\n" - "name = \"%1\"\n" - "extends = \"%2\"\n" - "description = \"%3\"\n") - .arg(tomlEscape(result.newName), - tomlEscape(parent.name), - tomlEscape(description)); - const QByteArray payload = body.toUtf8(); - if (f.write(payload) != payload.size() || !f.commit()) { - result.error = trUser("Failed to write %1: %2").arg(result.filePath, f.errorString()); - return result; - } - result.ok = true; - return result; -} - -} // namespace QodeAssist::Settings diff --git a/settings/AgentDuplicator.hpp b/settings/AgentDuplicator.hpp deleted file mode 100644 index c4ef751..0000000 --- a/settings/AgentDuplicator.hpp +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -namespace QodeAssist { -class AgentFactory; -struct AgentConfig; -} - -namespace QodeAssist::Settings { - -struct AgentDuplicateResult -{ - bool ok = false; - QString filePath; - QString newName; - QString error; -}; - -AgentDuplicateResult duplicateAgentInUserDir( - const AgentConfig &parent, const AgentFactory &factory); - -} // namespace QodeAssist::Settings diff --git a/settings/AgentListItem.cpp b/settings/AgentListItem.cpp deleted file mode 100644 index 20dec13..0000000 --- a/settings/AgentListItem.cpp +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentListItem.hpp" - -#include "SettingsTheme.hpp" -#include "TagChip.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -AgentListItem::AgentListItem(const AgentConfig &cfg, QWidget *parent) - : QFrame(parent) - , m_name(cfg.name) -{ - setObjectName(QStringLiteral("AgentListItem")); - setFrameShape(QFrame::NoFrame); - setAutoFillBackground(true); - setCursor(Qt::PointingHandCursor); - - auto *dot = new QLabel(QStringLiteral("●"), this); - QFont df = dot->font(); - df.setPixelSize(10); - dot->setFont(df); - QPalette dp = dot->palette(); - dp.setColor(QPalette::WindowText, dp.color(QPalette::Mid)); - dot->setPalette(dp); - - auto *nameLbl = new QLabel(cfg.name, this); - QFont nf = nameLbl->font(); - nf.setBold(true); - nf.setPixelSize(12); - nameLbl->setFont(nf); - - auto *headerRow = new QHBoxLayout; - headerRow->setContentsMargins(0, 0, 0, 0); - headerRow->setSpacing(6); - headerRow->addWidget(dot, 0, Qt::AlignVCenter); - headerRow->addWidget(nameLbl, 1); - - auto *col = new QVBoxLayout; - col->setContentsMargins(0, 0, 0, 0); - col->setSpacing(2); - col->addLayout(headerRow); - - if (!cfg.model.isEmpty()) { - auto *model = new QLabel(cfg.model, this); - model->setFont(monospaceFont(11)); - model->setContentsMargins(16, 0, 0, 0); - QPalette mp = model->palette(); - mp.setColor(QPalette::WindowText, mp.color(QPalette::Mid)); - model->setPalette(mp); - col->addWidget(model); - } - - if (!cfg.tags.isEmpty()) { - auto *tagsHolder = new QWidget(this); - auto *tagsLay = new QHBoxLayout(tagsHolder); - tagsLay->setContentsMargins(16, 2, 0, 0); - tagsLay->setSpacing(3); - for (const QString &t : cfg.tags) { - auto *chip = new TagChip(t, -1, tagsHolder); - connect(chip, &TagChip::clicked, this, &AgentListItem::tagClicked); - m_chips.append(chip); - tagsLay->addWidget(chip); - } - tagsLay->addStretch(1); - col->addWidget(tagsHolder); - } - - auto *outer = new QVBoxLayout(this); - outer->setContentsMargins(8, 6, 8, 6); - outer->setSpacing(0); - outer->addLayout(col); - - applyTheme(); -} - -void AgentListItem::setSelected(bool selected) -{ - if (m_selected == selected) - return; - m_selected = selected; - applyTheme(); -} - -void AgentListItem::setActiveTags(const QSet &active) -{ - for (auto *chip : m_chips) - chip->setActive(active.contains(chip->tag())); -} - -void AgentListItem::mouseReleaseEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - emit clicked(m_name); - QFrame::mouseReleaseEvent(event); -} - -void AgentListItem::changeEvent(QEvent *event) -{ - QFrame::changeEvent(event); - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) - applyTheme(); -} - -void AgentListItem::applyTheme() -{ - if (m_inApplyTheme) - return; - QScopedValueRollback guard(m_inApplyTheme, true); - const Theme theme = themeFor(palette()); - setStyleSheet(QStringLiteral( - "#AgentListItem { background:%1; border-top:1px solid %2; }") - .arg(m_selected ? theme.rowSelectedBg : QStringLiteral("transparent"), - theme.rowSeparator)); -} - -} // namespace QodeAssist::Settings diff --git a/settings/AgentListItem.hpp b/settings/AgentListItem.hpp deleted file mode 100644 index 219846d..0000000 --- a/settings/AgentListItem.hpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include - -#include - -namespace QodeAssist::Settings { - -class TagChip; - -class AgentListItem : public QFrame -{ - Q_OBJECT -public: - explicit AgentListItem(const AgentConfig &cfg, QWidget *parent = nullptr); - - QString agentName() const { return m_name; } - void setSelected(bool selected); - void setActiveTags(const QSet &active); - -signals: - void clicked(const QString &name); - void tagClicked(const QString &tag); - -protected: - void mouseReleaseEvent(QMouseEvent *event) override; - void changeEvent(QEvent *event) override; - -private: - void applyTheme(); - - QString m_name; - bool m_selected = false; - bool m_inApplyTheme = false; - QList m_chips; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/AgentListPane.cpp b/settings/AgentListPane.cpp deleted file mode 100644 index 14c9470..0000000 --- a/settings/AgentListPane.cpp +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentListPane.hpp" - -#include "AgentListItem.hpp" -#include "SettingsTheme.hpp" -#include "SettingsUiBuilders.hpp" -#include "TagFilterStrip.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent) - : QFrame(parent) - , m_factory(factory) -{ - setFrameShape(QFrame::StyledPanel); - - m_filterEdit = new QLineEdit(this); - m_filterEdit->setPlaceholderText(tr("Filter agents…")); - m_filterEdit->setClearButtonEnabled(true); - - auto *filterRow = new QHBoxLayout; - filterRow->setContentsMargins(6, 6, 6, 6); - filterRow->addWidget(m_filterEdit, 1); - m_filterHolder = new QWidget(this); - m_filterHolder->setObjectName(QStringLiteral("FilterHolder")); - m_filterHolder->setLayout(filterRow); - m_filterHolder->setAutoFillBackground(true); - - m_tagStrip = new TagFilterStrip(this); - - m_listScroll = new QScrollArea(this); - m_listScroll->setWidgetResizable(true); - m_listScroll->setFrameShape(QFrame::NoFrame); - m_listScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - auto *outer = new QVBoxLayout(this); - outer->setContentsMargins(0, 0, 0, 0); - outer->setSpacing(0); - outer->addWidget(m_filterHolder); - outer->addWidget(m_tagStrip); - outer->addWidget(m_listScroll, 1); - - m_filterDebounce = new QTimer(this); - m_filterDebounce->setSingleShot(true); - m_filterDebounce->setInterval(100); - connect(m_filterDebounce, &QTimer::timeout, this, &AgentListPane::rebuildList); - connect(m_filterEdit, &QLineEdit::textChanged, this, - [this](const QString &) { m_filterDebounce->start(); }); - - connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this, - [this](const QSet &) { rebuildList(); }, - Qt::QueuedConnection); - - applyFilterHolderTheme(); -} - -void AgentListPane::selectByName(const QString &name) -{ - if (name.isEmpty()) - return; - setCurrentNameInternal(name, false); - rebuildList(); - for (auto *item : m_rows) { - if (item->agentName() == name) { - QTimer::singleShot(0, this, [this, item] { - m_listScroll->ensureWidgetVisible(item, 0, 60); - }); - break; - } - } -} - -void AgentListPane::refresh() -{ - QMap counts; - for (const auto *a : visibleAgents()) - for (const QString &t : a->tags) - counts[t] += 1; - m_tagStrip->setAvailableTags(counts); - rebuildList(); -} - -void AgentListPane::changeEvent(QEvent *event) -{ - QFrame::changeEvent(event); - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) - applyFilterHolderTheme(); -} - -void AgentListPane::applyFilterHolderTheme() -{ - if (!m_filterHolder) - return; - const Theme theme = themeFor(palette()); - m_filterHolder->setStyleSheet( - QStringLiteral("QWidget#FilterHolder { background:%1;" - " border-bottom:1px solid %2; }") - .arg(theme.listHeaderBg, theme.rowSeparator)); -} - -std::vector AgentListPane::visibleAgents() const -{ - std::vector out; - if (!m_factory) - return out; - for (const auto &a : m_factory->configs()) { - if (a.hidden) - continue; - out.push_back(&a); - } - return out; -} - -bool AgentListPane::matchesFilters(const AgentConfig &a, const QString &lowerFilter) const -{ - if (!lowerFilter.isEmpty() - && !(a.name + QLatin1Char(' ') + a.model).toLower().contains(lowerFilter)) - return false; - const QSet &active = m_tagStrip->activeTags(); - for (const QString &t : active) - if (!a.tags.contains(t)) - return false; - return true; -} - -void AgentListPane::rebuildList() -{ - const QString lowerFilter = m_filterEdit->text().trimmed().toLower(); - - std::vector userAgents; - std::vector bundledAgents; - for (const auto *a : visibleAgents()) { - if (!matchesFilters(*a, lowerFilter)) - continue; - if (a->isUserSource()) - userAgents.push_back(a); - else - bundledAgents.push_back(a); - } - auto byName = [](const AgentConfig *a, const AgentConfig *b) { - return a->name.localeAwareCompare(b->name) < 0; - }; - std::sort(userAgents.begin(), userAgents.end(), byName); - std::sort(bundledAgents.begin(), bundledAgents.end(), byName); - - QList newRows; - auto *content = new QWidget; - content->setAutoFillBackground(true); - auto *contentLayout = new QVBoxLayout(content); - contentLayout->setContentsMargins(0, 0, 0, 0); - contentLayout->setSpacing(0); - - const QSet &activeTags = m_tagStrip->activeTags(); - auto addAgents = [&](const std::vector &agents) { - for (const AgentConfig *cfg : agents) { - auto *item = new AgentListItem(*cfg, content); - item->setSelected(cfg->name == m_currentName); - item->setActiveTags(activeTags); - connect(item, &AgentListItem::clicked, this, &AgentListPane::onRowClicked); - connect(item, &AgentListItem::tagClicked, this, - [this](const QString &) { refresh(); }, - Qt::QueuedConnection); - contentLayout->addWidget(item); - newRows.append(item); - } - }; - - if (!userAgents.empty()) { - contentLayout->addWidget(makeSectionHeader(tr("User"), content)); - addAgents(userAgents); - } - if (!bundledAgents.empty()) { - contentLayout->addWidget(makeSectionHeader(tr("Bundled"), content)); - addAgents(bundledAgents); - } - if (newRows.isEmpty()) { - auto *empty = new QLabel(tr("No agents match these filters."), content); - empty->setAlignment(Qt::AlignCenter); - empty->setContentsMargins(10, 16, 10, 16); - QPalette ep = empty->palette(); - ep.setColor(QPalette::WindowText, ep.color(QPalette::Mid)); - empty->setPalette(ep); - contentLayout->addWidget(empty); - } - contentLayout->addStretch(1); - - m_rows = newRows; - m_listScroll->setWidget(content); - - const AgentConfig *current - = m_currentName.isEmpty() || !m_factory - ? nullptr - : m_factory->configByName(m_currentName); - if (!current && !m_rows.isEmpty()) { - const QString fallback = m_rows.front()->agentName(); - m_rows.front()->setSelected(true); - setCurrentNameInternal(fallback, /*emitSignal*/ true); - return; - } - emit currentAgentChanged(m_currentName); -} - -void AgentListPane::onRowClicked(const QString &name) -{ - setCurrentNameInternal(name, /*emitSignal*/ true); -} - -void AgentListPane::setCurrentNameInternal(const QString &name, bool emitSignal) -{ - if (name == m_currentName) - return; - m_currentName = name; - for (auto *item : m_rows) - item->setSelected(item->agentName() == name); - if (emitSignal) - emit currentAgentChanged(m_currentName); -} - -} // namespace QodeAssist::Settings diff --git a/settings/AgentListPane.hpp b/settings/AgentListPane.hpp deleted file mode 100644 index 3182d2d..0000000 --- a/settings/AgentListPane.hpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include -#include - -#include - -class QLineEdit; -class QScrollArea; -class QTimer; -class QVBoxLayout; - -namespace QodeAssist { -class AgentFactory; -} - -namespace QodeAssist::Settings { - -class AgentListItem; -class TagFilterStrip; - -class AgentListPane : public QFrame -{ - Q_OBJECT -public: - explicit AgentListPane(AgentFactory *factory, QWidget *parent = nullptr); - - QString currentName() const { return m_currentName; } - void selectByName(const QString &name); - void refresh(); - -signals: - void currentAgentChanged(const QString &name); - -protected: - void changeEvent(QEvent *event) override; - -private: - void rebuildList(); - void applyFilterHolderTheme(); - bool matchesFilters(const AgentConfig &a, const QString &lowerFilter) const; - std::vector visibleAgents() const; - void setCurrentNameInternal(const QString &name, bool emitSignal); - void onRowClicked(const QString &name); - - AgentFactory *m_factory; - QLineEdit *m_filterEdit = nullptr; - QTimer *m_filterDebounce = nullptr; - QWidget *m_filterHolder = nullptr; - TagFilterStrip *m_tagStrip = nullptr; - QScrollArea *m_listScroll = nullptr; - QList m_rows; - QString m_currentName; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/AgentsSettingsPage.cpp b/settings/AgentsSettingsPage.cpp deleted file mode 100644 index c66adb5..0000000 --- a/settings/AgentsSettingsPage.cpp +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentsSettingsPage.hpp" - -#include "AgentDetailPane.hpp" -#include "AgentDuplicator.hpp" -#include "AgentListPane.hpp" -#include "SettingsTheme.hpp" -#include "SettingsConstants.hpp" - -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace QodeAssist::Settings { - -AgentsPageNavigator::AgentsPageNavigator(QObject *parent) - : QObject(parent) -{} - -void AgentsPageNavigator::requestSelectAgent(const QString &name) -{ - m_pending = name; - emit selectAgentRequested(name); -} - -QString AgentsPageNavigator::takePendingSelection() -{ - QString p = m_pending; - m_pending.clear(); - return p; -} - -namespace { - -class AgentsWidget : public Core::IOptionsPageWidget -{ - Q_OBJECT -public: - explicit AgentsWidget(AgentFactory *agentFactory, AgentsPageNavigator *navigator) - : m_agentFactory(agentFactory) - , m_navigator(navigator) - { - Q_ASSERT(m_agentFactory); - - m_titleLabel = new QLabel(tr("Agents"), this); - QFont tf = m_titleLabel->font(); - tf.setBold(true); - tf.setPixelSize(13); - m_titleLabel->setFont(tf); - - m_reload = new QPushButton(tr("Reload from disk"), this); - m_openUserDir = new QPushButton(tr("Open agents folder"), this); - - m_userPathLabel = new QLabel(this); - m_userPathLabel->setFont(monospaceFont(11)); - QPalette mutedPal = m_userPathLabel->palette(); - mutedPal.setColor(QPalette::WindowText, mutedPal.color(QPalette::Mid)); - m_userPathLabel->setPalette(mutedPal); - m_userPathLabel->setMaximumWidth(260); - - auto *headerRow = new QHBoxLayout; - headerRow->setContentsMargins(0, 0, 0, 0); - headerRow->setSpacing(8); - headerRow->addWidget(m_titleLabel); - headerRow->addStretch(1); - headerRow->addWidget(m_reload); - headerRow->addWidget(m_userPathLabel); - headerRow->addWidget(m_openUserDir); - - auto *headerSep = new QFrame(this); - headerSep->setFrameShape(QFrame::HLine); - headerSep->setFrameShadow(QFrame::Sunken); - - m_listPane = new AgentListPane(m_agentFactory, this); - - m_detail = new AgentDetailPane(this); - m_detail->setInstanceFactory(m_agentFactory->instanceFactory()); - m_detailScroll = new QScrollArea(this); - m_detailScroll->setWidgetResizable(true); - m_detailScroll->setFrameShape(QFrame::StyledPanel); - m_detailScroll->setWidget(m_detail); - - auto *splitter = new QSplitter(Qt::Horizontal, this); - splitter->addWidget(m_listPane); - splitter->addWidget(m_detailScroll); - splitter->setStretchFactor(0, 0); - splitter->setStretchFactor(1, 1); - splitter->setSizes({320, 700}); - - auto *root = new QVBoxLayout(this); - root->setContentsMargins(8, 8, 8, 8); - root->setSpacing(6); - root->addLayout(headerRow); - root->addWidget(headerSep); - root->addWidget(splitter, 1); - - connect(m_reload, &QPushButton::clicked, this, &AgentsWidget::reloadFromDisk); - connect(m_openUserDir, &QPushButton::clicked, this, [] { - const QString dir = QodeAssist::AgentFactory::userAgentsDir(); - QDir().mkpath(dir); - QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); - }); - - connect(m_listPane, &AgentListPane::currentAgentChanged, this, - [this](const QString &name) { - if (const AgentConfig *cfg = m_agentFactory->configByName(name)) - m_detail->setAgent(*cfg); - else - m_detail->clear(); - }); - - connect(m_detail, &AgentDetailPane::openInEditorRequested, - this, &AgentsWidget::openAgentInEditor); - connect(m_detail, &AgentDetailPane::customizeRequested, - this, &AgentsWidget::customizeAgent); - connect(m_detail, &AgentDetailPane::deleteRequested, - this, &AgentsWidget::deleteAgent); - - if (m_navigator) { - connect(m_navigator, &AgentsPageNavigator::selectAgentRequested, - m_listPane, &AgentListPane::selectByName); - } - - reloadFromDisk(); - - if (m_navigator) { - QTimer::singleShot(0, this, [this] { - if (!m_navigator) - return; - const QString pending = m_navigator->takePendingSelection(); - if (!pending.isEmpty()) - m_listPane->selectByName(pending); - }); - } - } - - void apply() final {} - -private: - void reloadFromDisk() - { - m_agentFactory->reload(); - m_detail->setLoadDiagnostics( - m_agentFactory->lastLoadErrors(), m_agentFactory->lastLoadWarnings()); - updateUserPathLabel(); - m_listPane->refresh(); - } - - void updateUserPathLabel() - { - const QString dir = QodeAssist::AgentFactory::userAgentsDir(); - m_userPathLabel->setText( - QFontMetrics(m_userPathLabel->font()).elidedText(dir, Qt::ElideLeft, 256)); - m_userPathLabel->setToolTip(dir); - } - - void openAgentInEditor(const AgentConfig &agent) - { - const QString name = agent.name; - const QString sourcePath = agent.sourcePath; - const bool isUser = agent.isUserSource(); - - if (!isUser) { - QMessageBox::information( - this, tr("Open agent"), - tr("'%1' is bundled with the plugin and read-only.\n" - "Use Duplicate to create an editable user copy.") - .arg(name)); - return; - } - if (sourcePath.isEmpty() || sourcePath.startsWith(QLatin1String(":/"))) { - QMessageBox::warning( - this, tr("Open agent"), - tr("Agent '%1' has no editable source file.").arg(name)); - return; - } - if (!Core::EditorManager::openEditor(Utils::FilePath::fromString(sourcePath))) { - QMessageBox::warning( - this, tr("Open agent"), - tr("Could not open %1.").arg(sourcePath)); - } - } - - void customizeAgent(const AgentConfig &parent) - { - const AgentDuplicateResult res = duplicateAgentInUserDir(parent, *m_agentFactory); - if (!res.ok) { - QMessageBox::warning(this, tr("Duplicate"), res.error); - return; - } - const QString newName = res.newName; - reloadFromDisk(); - m_listPane->selectByName(newName); - } - - void deleteAgent(const AgentConfig &agent) - { - if (!agent.isUserSource()) - return; - const QString name = agent.name; - const QString sourcePath = agent.sourcePath; - - if (QMessageBox::question( - this, tr("Delete Agent"), - tr("Delete agent '%1'?\n\nThis will remove the file:\n%2") - .arg(name, sourcePath), - QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - != QMessageBox::Yes) - return; - if (!QFile::remove(sourcePath)) { - QMessageBox::warning( - this, tr("Delete Agent"), - tr("Could not delete the agent file:\n%1").arg(sourcePath)); - return; - } - reloadFromDisk(); - } - - AgentFactory *m_agentFactory; - QPointer m_navigator; - - QLabel *m_titleLabel = nullptr; - QPushButton *m_reload = nullptr; - QPushButton *m_openUserDir = nullptr; - QLabel *m_userPathLabel = nullptr; - - AgentListPane *m_listPane = nullptr; - QScrollArea *m_detailScroll = nullptr; - AgentDetailPane *m_detail = nullptr; -}; - -class AgentsSettingsPage : public Core::IOptionsPage -{ -public: - AgentsSettingsPage(AgentFactory *agentFactory, AgentsPageNavigator *navigator) - { - setId(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID); - setDisplayName(QObject::tr("Agents")); - setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY); - setWidgetCreator([agentFactory, navigator]() { - return new AgentsWidget(agentFactory, navigator); - }); - } -}; - -} // namespace - -std::unique_ptr createAgentsSettingsPage( - AgentFactory *agentFactory, AgentsPageNavigator *navigator) -{ - return std::make_unique(agentFactory, navigator); -} - -} // namespace QodeAssist::Settings - -#include "AgentsSettingsPage.moc" diff --git a/settings/AgentsSettingsPage.hpp b/settings/AgentsSettingsPage.hpp deleted file mode 100644 index 9fbcfb9..0000000 --- a/settings/AgentsSettingsPage.hpp +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include -#include - -namespace Core { class IOptionsPage; } - -namespace QodeAssist { -class AgentFactory; -} - -namespace QodeAssist::Settings { - -class AgentsPageNavigator : public QObject -{ - Q_OBJECT -public: - explicit AgentsPageNavigator(QObject *parent = nullptr); - - void requestSelectAgent(const QString &name); - QString takePendingSelection(); - -signals: - void selectAgentRequested(const QString &name); - -private: - QString m_pending; -}; - -std::unique_ptr createAgentsSettingsPage( - AgentFactory *agentFactory, AgentsPageNavigator *navigator); - -} // namespace QodeAssist::Settings diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index e311bc4..757bb64 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -16,24 +16,11 @@ add_library(QodeAssistSettings STATIC ProjectSettingsPanel.hpp ProjectSettingsPanel.cpp ProviderSettings.hpp ProviderSettings.cpp ProviderNameMigration.hpp - ProvidersSettingsPage.hpp ProvidersSettingsPage.cpp - SettingsTheme.hpp - SettingsUiBuilders.hpp SettingsUiBuilders.cpp - SectionBox.hpp SectionBox.cpp - TagChip.hpp TagChip.cpp - ProviderListItem.hpp ProviderListItem.cpp - ProviderDetailPane.hpp ProviderDetailPane.cpp PluginUpdater.hpp PluginUpdater.cpp UpdateDialog.hpp UpdateDialog.cpp AgentRole.hpp AgentRole.cpp AgentRoleDialog.hpp AgentRoleDialog.cpp AgentRolesWidget.hpp AgentRolesWidget.cpp - AgentsSettingsPage.hpp AgentsSettingsPage.cpp - AgentDetailPane.hpp AgentDetailPane.cpp - AgentListItem.hpp AgentListItem.cpp - AgentListPane.hpp AgentListPane.cpp - AgentDuplicator.hpp AgentDuplicator.cpp - TagFilterStrip.hpp TagFilterStrip.cpp ) target_link_libraries(QodeAssistSettings @@ -44,8 +31,6 @@ target_link_libraries(QodeAssistSettings QtCreator::Core QtCreator::Utils QodeAssistLogger - ProvidersConfig - Agents Skills ) target_include_directories(QodeAssistSettings PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/settings/ProviderDetailPane.cpp b/settings/ProviderDetailPane.cpp deleted file mode 100644 index 4d49756..0000000 --- a/settings/ProviderDetailPane.cpp +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "ProviderDetailPane.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "ProviderInstanceWriter.hpp" -#include "SectionBox.hpp" -#include "SettingsTheme.hpp" -#include "SettingsUiBuilders.hpp" - -namespace QodeAssist::Settings { - -ProviderDetailPane::ProviderDetailPane(QWidget *parent) - : QWidget(parent) -{ - m_nameLabel = new QLabel(this); - QFont nf = m_nameLabel->font(); - nf.setBold(true); - nf.setPixelSize(15); - m_nameLabel->setFont(nf); - - m_sourcePathLabel = new QLabel(this); - m_sourcePathLabel->setFont(monospaceFont(11)); - QPalette spp = m_sourcePathLabel->palette(); - spp.setColor(QPalette::WindowText, spp.color(QPalette::Mid)); - m_sourcePathLabel->setPalette(spp); - - m_editBtn = new QPushButton(tr("Edit…"), this); - m_editBtn->setDefault(true); - m_openInEditorBtn = new QPushButton(tr("Open in editor"), this); - m_openInEditorBtn->setToolTip( - tr("Open this provider's TOML file in Qt Creator. " - "Bundled providers are read-only — duplicate first.")); - m_dupBtn = new QPushButton(tr("Duplicate…"), this); - m_deleteBtn = new QPushButton(tr("Delete"), this); - m_cancelBtn = new QPushButton(tr("Cancel"), this); - m_saveBtn = new QPushButton(tr("Save"), this); - m_saveBtn->setDefault(true); - m_cancelBtn->hide(); - m_saveBtn->hide(); - - connect(m_editBtn, &QPushButton::clicked, this, [this] { setEditing(true); }); - connect(m_cancelBtn, &QPushButton::clicked, this, [this] { - setEditing(false); - populate(m_current, m_currentHasStoredKey); - }); - connect(m_saveBtn, &QPushButton::clicked, this, [this] { - emit saveRequested(collectEdits()); - }); - connect(m_openInEditorBtn, &QPushButton::clicked, this, - [this] { emit openInEditorRequested(m_current.sourcePath); }); - connect(m_dupBtn, &QPushButton::clicked, this, [this] { emit duplicateRequested(); }); - connect(m_deleteBtn, &QPushButton::clicked, this, [this] { emit deleteRequested(); }); - - auto *btnBar = new QHBoxLayout; - btnBar->setContentsMargins(0, 0, 0, 0); - btnBar->setSpacing(4); - btnBar->addWidget(m_editBtn); - btnBar->addWidget(m_openInEditorBtn); - btnBar->addWidget(m_dupBtn); - btnBar->addWidget(m_deleteBtn); - btnBar->addWidget(m_cancelBtn); - btnBar->addWidget(m_saveBtn); - - auto *titleRow = new QHBoxLayout; - titleRow->setContentsMargins(0, 0, 0, 0); - titleRow->setSpacing(8); - titleRow->addWidget(m_nameLabel); - titleRow->addStretch(1); - - auto *headerLeft = new QVBoxLayout; - headerLeft->setContentsMargins(0, 0, 0, 0); - headerLeft->setSpacing(2); - headerLeft->addLayout(titleRow); - headerLeft->addWidget(m_sourcePathLabel); - - auto *headerRow = new QHBoxLayout; - headerRow->setContentsMargins(0, 0, 0, 0); - headerRow->setSpacing(8); - headerRow->addLayout(headerLeft, 1); - headerRow->addLayout(btnBar); - - auto *headerSep = new QFrame(this); - headerSep->setFrameShape(QFrame::HLine); - headerSep->setFrameShadow(QFrame::Sunken); - - m_descriptionLabel = new QLabel(this); - m_descriptionLabel->setWordWrap(true); - m_descriptionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - - auto *identitySection = new SectionBox(tr("Identity"), this); - m_nameEdit = new QLineEdit(this); - m_typeEdit = new QLineEdit(this); - m_typeEdit->setReadOnly(true); - m_descriptionEdit = new QPlainTextEdit(this); - m_descriptionEdit->setMaximumHeight(60); - m_descriptionEdit->setReadOnly(true); - auto *identityGrid = new QGridLayout; - identityGrid->setContentsMargins(0, 0, 0, 0); - identityGrid->setHorizontalSpacing(8); - identityGrid->setVerticalSpacing(4); - FormBuilder(identityGrid) - .row(tr("Name:"), m_nameEdit) - .row(tr("Client API:"), m_typeEdit, - tr("The client API this provider speaks. " - "Cannot be changed after creation.")) - .row(tr("Description:"), m_descriptionEdit); - identitySection->bodyLayout()->addLayout(identityGrid); - - auto *endpointSection = new SectionBox(tr("Endpoint"), this); - m_urlEdit = new QLineEdit(this); - m_urlEdit->setFont(monospaceFont(11)); - auto *endpointGrid = new QGridLayout; - endpointGrid->setContentsMargins(0, 0, 0, 0); - endpointGrid->setHorizontalSpacing(8); - endpointGrid->setVerticalSpacing(4); - FormBuilder(endpointGrid).row(tr("URL:"), m_urlEdit, - tr("Base URL. Agents append their endpoint path " - "(e.g. /chat/completions) to this.")); - endpointSection->bodyLayout()->addLayout(endpointGrid); - - m_samplePreview = new QLabel(this); - m_samplePreview->setFont(monospaceFont(11)); - m_samplePreview->setTextInteractionFlags(Qt::TextSelectableByMouse); - m_samplePreview->setWordWrap(true); - m_samplePreview->setContentsMargins(6, 4, 6, 4); - m_samplePreview->setAutoFillBackground(true); - endpointSection->bodyLayout()->addWidget(m_samplePreview); - - auto *credSection = new SectionBox(tr("Credentials"), this); - m_apiKeyEdit = new QLineEdit(this); - m_apiKeyEdit->setEchoMode(QLineEdit::Password); - m_apiKeyEdit->setPlaceholderText(tr("Enter API key…")); - m_revealKeyBtn = new QToolButton(this); - m_revealKeyBtn->setText(QStringLiteral("👁")); - m_revealKeyBtn->setCheckable(true); - m_revealKeyBtn->setToolTip(tr("Show / hide API key")); - connect(m_revealKeyBtn, &QToolButton::toggled, this, [this](bool on) { - m_apiKeyEdit->setEchoMode(on ? QLineEdit::Normal : QLineEdit::Password); - }); - m_apiKeySaveBtn = new QPushButton(tr("Save key"), this); - m_apiKeySaveBtn->setEnabled(false); - m_apiKeyClearBtn = new QPushButton(tr("Clear"), this); - m_apiKeyClearBtn->setToolTip(tr("Erase the stored API key for this provider")); - connect(m_apiKeyEdit, &QLineEdit::textChanged, this, [this](const QString &t) { - m_apiKeySaveBtn->setEnabled(!t.isEmpty()); - }); - connect(m_apiKeyEdit, &QLineEdit::returnPressed, this, [this] { - if (!m_apiKeyEdit->text().isEmpty()) - m_apiKeySaveBtn->click(); - }); - connect(m_apiKeySaveBtn, &QPushButton::clicked, this, [this] { - const QString key = m_apiKeyEdit->text(); - if (key.isEmpty()) - return; - emit apiKeySaveRequested(key); - m_apiKeyEdit->clear(); - }); - connect(m_apiKeyClearBtn, &QPushButton::clicked, this, - [this] { emit apiKeyClearRequested(); }); - m_keyHint = makeHintLabel(QString{}, this); - - auto *keyRow = new QHBoxLayout; - keyRow->setContentsMargins(0, 0, 0, 0); - keyRow->setSpacing(4); - keyRow->addWidget(m_apiKeyEdit, 1); - keyRow->addWidget(m_revealKeyBtn); - keyRow->addWidget(m_apiKeySaveBtn); - keyRow->addWidget(m_apiKeyClearBtn); - - auto *credGrid = new QGridLayout; - credGrid->setContentsMargins(0, 0, 0, 0); - credGrid->setHorizontalSpacing(8); - credGrid->setVerticalSpacing(4); - FormBuilder credForm(credGrid); - credForm.row(tr("API key:"), keyRow); - credGrid->addWidget(m_keyHint, credForm.currentRow(), 1); - credSection->bodyLayout()->addLayout(credGrid); - - m_launchSection = new SectionBox(tr("Launch"), this); - m_launchEmptyHint = new QLabel(this); - m_launchEmptyHint->setWordWrap(true); - QPalette lehp = m_launchEmptyHint->palette(); - lehp.setColor(QPalette::WindowText, lehp.color(QPalette::Mid)); - m_launchEmptyHint->setPalette(lehp); - m_launchCmdLabel = new QLabel(this); - m_launchCmdLabel->setFont(monospaceFont(11)); - m_launchCmdLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); - m_launchCmdLabel->setWordWrap(true); - m_launchStatusPill = new QLabel(tr("idle"), this); - m_startBtn = new QPushButton(tr("Start"), this); - m_stopBtn = new QPushButton(tr("Stop"), this); - m_restartBtn = new QPushButton(tr("Restart"), this); - connect(m_startBtn, &QPushButton::clicked, this, - [this] { emit launchStartRequested(m_current.name); }); - connect(m_stopBtn, &QPushButton::clicked, this, - [this] { emit launchStopRequested(m_current.name); }); - connect(m_restartBtn, &QPushButton::clicked, this, - [this] { emit launchRestartRequested(m_current.name); }); - auto *launchBtnRow = new QHBoxLayout; - launchBtnRow->setContentsMargins(0, 0, 0, 0); - launchBtnRow->setSpacing(6); - launchBtnRow->addWidget(m_launchStatusPill); - launchBtnRow->addStretch(1); - launchBtnRow->addWidget(m_startBtn); - launchBtnRow->addWidget(m_stopBtn); - launchBtnRow->addWidget(m_restartBtn); - - m_launchTerminalToggle = new QToolButton(this); - m_launchTerminalToggle->setText(tr("▸ Show launch terminal")); - m_launchTerminalToggle->setCursor(Qt::PointingHandCursor); - m_launchTerminalToggle->setAutoRaise(true); - m_launchTerminalToggle->setCheckable(true); - m_launchTerminal = new TerminalSolution::TerminalView(this); - { - QFont termFont(TerminalSolution::defaultFontFamily()); - const int sz = TerminalSolution::defaultFontSize(); - if (sz > 0) - termFont.setPixelSize(sz); - termFont.setStyleHint(QFont::Monospace); - m_launchTerminal->setFont(termFont); - applyTerminalPalette(); - } - m_launchTerminal->setMinimumHeight(180); - m_launchTerminal->setVisible(false); - connect(m_launchTerminalToggle, &QToolButton::toggled, this, [this](bool on) { - m_launchTerminal->setVisible(on); - m_launchTerminalToggle->setText( - on ? tr("▾ Hide launch terminal") : tr("▸ Show launch terminal")); - }); - - m_launchSection->bodyLayout()->addWidget(m_launchEmptyHint); - m_launchSection->bodyLayout()->addWidget(m_launchCmdLabel); - m_launchSection->bodyLayout()->addLayout(launchBtnRow); - m_launchSection->bodyLayout()->addWidget(m_launchTerminalToggle, 0, Qt::AlignLeft); - m_launchSection->bodyLayout()->addWidget(m_launchTerminal); - - m_rawToggle = new QToolButton(this); - m_rawToggle->setText(tr("▸ Show raw TOML")); - m_rawToggle->setCursor(Qt::PointingHandCursor); - m_rawToggle->setAutoRaise(true); - m_rawToggle->setCheckable(true); - m_rawToml = new QPlainTextEdit(this); - m_rawToml->setReadOnly(true); - m_rawToml->setFont(monospaceFont(11)); - m_rawToml->setMinimumHeight(120); - m_rawToml->setVisible(false); - connect(m_rawToggle, &QToolButton::toggled, this, [this](bool on) { - m_rawToml->setVisible(on); - m_rawToggle->setText(on ? tr("▾ Hide raw TOML") : tr("▸ Show raw TOML")); - }); - - auto *root = new QVBoxLayout(this); - root->setContentsMargins(12, 12, 12, 12); - root->setSpacing(10); - root->addLayout(headerRow); - root->addWidget(headerSep); - root->addWidget(m_descriptionLabel); - root->addWidget(identitySection); - root->addWidget(endpointSection); - root->addWidget(credSection); - root->addWidget(m_launchSection); - root->addWidget(m_rawToggle, 0, Qt::AlignLeft); - root->addWidget(m_rawToml); - root->addStretch(1); - - clear(); -} - -void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool hasStoredKey) -{ - m_current = inst; - m_currentHasStoredKey = hasStoredKey; - const bool isUser = inst.isUserSource(); - const bool needsKey = !inst.apiKeyRef.isEmpty(); - - m_nameLabel->setText(inst.name); - m_sourcePathLabel->setText(inst.sourcePath); - - m_descriptionLabel->setText( - inst.description.isEmpty() ? tr("No description provided.") : inst.description); - - m_nameEdit->setText(inst.name); - m_typeEdit->setText(inst.clientApi); - m_descriptionEdit->setPlainText(inst.description); - m_urlEdit->setText(inst.url); - - m_apiKeyEdit->clear(); - m_apiKeyEdit->setEnabled(needsKey); - m_apiKeySaveBtn->setEnabled(false); - m_apiKeyClearBtn->setEnabled(needsKey && hasStoredKey); - m_revealKeyBtn->setEnabled(needsKey); - m_revealKeyBtn->setChecked(false); - if (!needsKey) { - m_apiKeyEdit->setPlaceholderText(tr("— not required (local provider)")); - m_keyHint->setText(tr("This provider type does not use a key.")); - } else if (hasStoredKey) { - m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it.")); - m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to " - "replace it, or Clear to erase it.")); - } else { - m_apiKeyEdit->setPlaceholderText(tr("Enter API key…")); - m_keyHint->setText(tr("No key stored yet. Type a key and press Save key.")); - } - - m_samplePreview->setText( - QStringLiteral("# sample request line\nPOST %1/").arg(inst.url)); - applyPreviewPalette(); - - m_deleteBtn->setEnabled(isUser); - m_dupBtn->setEnabled(true); - m_editBtn->setVisible(isUser); - m_openInEditorBtn->setEnabled(isUser); - setEditing(false); - - QString toml = QStringLiteral("# %1\n").arg(inst.sourcePath); - toml += Providers::ProviderInstanceWriter::toToml(inst); - m_rawToml->setPlainText(toml); -} - -void ProviderDetailPane::clear() -{ - m_current = {}; - m_nameLabel->setText(tr("Select a provider")); - m_sourcePathLabel->clear(); - m_descriptionLabel->clear(); - m_nameEdit->clear(); - m_typeEdit->clear(); - m_descriptionEdit->clear(); - m_urlEdit->clear(); - m_apiKeyEdit->clear(); - m_apiKeyEdit->setEnabled(false); - m_apiKeySaveBtn->setEnabled(false); - m_apiKeyClearBtn->setEnabled(false); - m_revealKeyBtn->setEnabled(false); - m_samplePreview->clear(); - m_rawToml->clear(); - m_editBtn->setVisible(false); - m_dupBtn->setEnabled(false); - m_deleteBtn->setEnabled(false); - m_openInEditorBtn->setEnabled(false); -} - -void ProviderDetailPane::refreshKeyStatus(bool hasStoredKey) -{ - m_currentHasStoredKey = hasStoredKey; - const bool needsKey = !m_current.apiKeyRef.isEmpty(); - m_apiKeyClearBtn->setEnabled(needsKey && hasStoredKey); - if (!needsKey) - return; - if (hasStoredKey) { - m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it.")); - m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to " - "replace it, or Clear to erase it.")); - } else { - m_apiKeyEdit->setPlaceholderText(tr("Enter API key…")); - m_keyHint->setText(tr("No key stored yet. Type a key and press Save key.")); - } -} - -void ProviderDetailPane::setLaunchState( - Providers::ProviderLauncher::State st, const QString &lastError) -{ - const bool hasLaunch = !m_current.launch.isEmpty(); - m_launchSection->setVisible(true); - m_launchEmptyHint->setVisible(!hasLaunch); - m_launchCmdLabel->setVisible(hasLaunch); - m_startBtn->setVisible(hasLaunch); - m_stopBtn->setVisible(hasLaunch); - m_restartBtn->setVisible(hasLaunch); - m_launchStatusPill->setVisible(hasLaunch); - m_launchTerminalToggle->setVisible(hasLaunch); - - if (!hasLaunch) { - m_launchEmptyHint->setText(tr( - "No [launch] block. This provider is treated as external — " - "the plugin will not spawn or supervise any process. " - "Add a [launch] block to the TOML to have the plugin manage " - "a local server here.")); - m_launchCmdLabel->clear(); - m_launchTerminal->clearContents(); - return; - } - - const QString detachedNote = m_current.launch.detach - ? tr(" (detached — survives Qt Creator restart)") - : QString(); - m_launchCmdLabel->setText( - QStringLiteral("%1 %2%3") - .arg(m_current.launch.command.toHtmlEscaped(), - m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(), - detachedNote)); - - QString statusText; - switch (st) { - case Providers::ProviderLauncher::Idle: statusText = tr("idle"); break; - case Providers::ProviderLauncher::Starting: statusText = tr("starting…"); break; - case Providers::ProviderLauncher::Probing: statusText = tr("probing…"); break; - case Providers::ProviderLauncher::Ready: statusText = tr("ready"); break; - case Providers::ProviderLauncher::Stopping: statusText = tr("stopping…"); break; - case Providers::ProviderLauncher::Failed: - statusText = lastError.isEmpty() ? tr("failed") - : tr("failed — %1").arg(lastError); - break; - } - m_launchStatusPill->setText(statusText); - - const bool running = st == Providers::ProviderLauncher::Starting - || st == Providers::ProviderLauncher::Probing - || st == Providers::ProviderLauncher::Ready; - m_startBtn->setEnabled(!running && st != Providers::ProviderLauncher::Stopping); - m_stopBtn->setEnabled(running); - m_restartBtn->setEnabled(running || st == Providers::ProviderLauncher::Failed); -} - -void ProviderDetailPane::resetLaunchTerminal(const QByteArray &scrollback) -{ - m_launchTerminal->clearContents(); - if (!scrollback.isEmpty()) - m_launchTerminal->writeToTerminal(scrollback, true); -} - -void ProviderDetailPane::appendLaunchBytes(const QByteArray &chunk) -{ - m_launchTerminal->writeToTerminal(chunk, true); -} - -void ProviderDetailPane::changeEvent(QEvent *event) -{ - QWidget::changeEvent(event); - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) { - applyPreviewPalette(); - applyTerminalPalette(); - } -} - -void ProviderDetailPane::setEditing(bool on) -{ - m_editing = on; - m_nameEdit->setReadOnly(!on); - m_descriptionEdit->setReadOnly(!on); - m_urlEdit->setReadOnly(!on); - m_editBtn->setVisible(!on && m_current.isUserSource()); - m_dupBtn->setVisible(!on); - m_deleteBtn->setVisible(!on); - m_cancelBtn->setVisible(on); - m_saveBtn->setVisible(on); -} - -Providers::ProviderInstance ProviderDetailPane::collectEdits() const -{ - Providers::ProviderInstance out = m_current; - out.name = m_nameEdit->text().trimmed(); - out.description = m_descriptionEdit->toPlainText().trimmed(); - out.url = m_urlEdit->text().trimmed(); - return out; -} - -void ProviderDetailPane::applyPreviewPalette() -{ - const Theme theme = themeFor(palette()); - m_samplePreview->setStyleSheet(QStringLiteral( - "QLabel { background:%1; border:1px solid %2; }") - .arg(theme.codeBg, theme.rowSeparator)); -} - -void ProviderDetailPane::applyTerminalPalette() -{ - if (!m_launchTerminal) - return; - const QPalette pal = palette(); - const bool dark = isDarkPalette(pal); - const std::array ansi = dark - ? std::array{ - QColor("#000000"), QColor("#cd3131"), QColor("#0dbc79"), - QColor("#e5e510"), QColor("#2472c8"), QColor("#bc3fbc"), - QColor("#11a8cd"), QColor("#e5e5e5"), - QColor("#666666"), QColor("#f14c4c"), QColor("#23d18b"), - QColor("#f5f543"), QColor("#3b8eea"), QColor("#d670d6"), - QColor("#29b8db"), QColor("#ffffff"), - } - : std::array{ - QColor("#000000"), QColor("#c91b00"), QColor("#00c200"), - QColor("#c7c400"), QColor("#0037da"), QColor("#c930c7"), - QColor("#00c5c7"), QColor("#c7c7c7"), - QColor("#676767"), QColor("#ff6d67"), QColor("#5ff967"), - QColor("#fefb67"), QColor("#6871ff"), QColor("#ff76ff"), - QColor("#5ffdff"), QColor("#ffffff"), - }; - std::array colors{}; - for (int i = 0; i < 16; ++i) - colors[i] = ansi[i]; - colors[16] = pal.color(QPalette::Text); - colors[17] = pal.color(QPalette::Base); - colors[18] = pal.color(QPalette::Highlight); - colors[19] = dark ? QColor("#5a5a40") : QColor("#fff59d"); - m_launchTerminal->setColors(colors); -} - -} // namespace QodeAssist::Settings diff --git a/settings/ProviderDetailPane.hpp b/settings/ProviderDetailPane.hpp deleted file mode 100644 index 97d30c1..0000000 --- a/settings/ProviderDetailPane.hpp +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include "ProviderInstance.hpp" -#include "ProviderLauncher.hpp" - -class QLabel; -class QLineEdit; -class QPlainTextEdit; -class QPushButton; -class QToolButton; - -namespace TerminalSolution { -class TerminalView; -} - -namespace QodeAssist::Settings { - -class SectionBox; - -class ProviderDetailPane : public QWidget -{ - Q_OBJECT -public: - explicit ProviderDetailPane(QWidget *parent = nullptr); - - void populate(const Providers::ProviderInstance &inst, bool hasStoredKey); - void clear(); - void refreshKeyStatus(bool hasStoredKey); - void setLaunchState(Providers::ProviderLauncher::State st, const QString &lastError); - void resetLaunchTerminal(const QByteArray &scrollback); - void appendLaunchBytes(const QByteArray &chunk); - - QString currentName() const { return m_current.name; } - -signals: - void saveRequested(const Providers::ProviderInstance &edited); - void duplicateRequested(); - void deleteRequested(); - void apiKeySaveRequested(const QString &newKey); - void apiKeyClearRequested(); - void launchStartRequested(const QString &providerName); - void launchStopRequested(const QString &providerName); - void launchRestartRequested(const QString &providerName); - void openInEditorRequested(const QString &sourcePath); - -protected: - void changeEvent(QEvent *event) override; - -private: - void setEditing(bool on); - Providers::ProviderInstance collectEdits() const; - void applyPreviewPalette(); - void applyTerminalPalette(); - - bool m_editing = false; - bool m_currentHasStoredKey = false; - Providers::ProviderInstance m_current; - - QLabel *m_nameLabel = nullptr; - QLabel *m_sourcePathLabel = nullptr; - QPushButton *m_editBtn = nullptr; - QPushButton *m_openInEditorBtn = nullptr; - QPushButton *m_dupBtn = nullptr; - QPushButton *m_deleteBtn = nullptr; - QPushButton *m_cancelBtn = nullptr; - QPushButton *m_saveBtn = nullptr; - - QLabel *m_descriptionLabel = nullptr; - - QLineEdit *m_nameEdit = nullptr; - QLineEdit *m_typeEdit = nullptr; - QPlainTextEdit *m_descriptionEdit = nullptr; - QLineEdit *m_urlEdit = nullptr; - QLabel *m_samplePreview = nullptr; - - QLineEdit *m_apiKeyEdit = nullptr; - QToolButton *m_revealKeyBtn = nullptr; - QLabel *m_keyHint = nullptr; - QPushButton *m_apiKeySaveBtn = nullptr; - QPushButton *m_apiKeyClearBtn = nullptr; - - SectionBox *m_launchSection = nullptr; - QLabel *m_launchEmptyHint = nullptr; - QLabel *m_launchCmdLabel = nullptr; - QLabel *m_launchStatusPill = nullptr; - QPushButton *m_startBtn = nullptr; - QPushButton *m_stopBtn = nullptr; - QPushButton *m_restartBtn = nullptr; - QToolButton *m_launchTerminalToggle = nullptr; - TerminalSolution::TerminalView *m_launchTerminal = nullptr; - - QToolButton *m_rawToggle = nullptr; - QPlainTextEdit *m_rawToml = nullptr; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/ProviderListItem.cpp b/settings/ProviderListItem.cpp deleted file mode 100644 index 26fcb89..0000000 --- a/settings/ProviderListItem.cpp +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "ProviderListItem.hpp" - -#include -#include -#include -#include -#include - -#include "ProviderInstance.hpp" -#include "SettingsTheme.hpp" - -namespace QodeAssist::Settings { - -ProviderListItem::ProviderListItem( - const Providers::ProviderInstance &inst, QWidget *parent) - : QFrame(parent) - , m_name(inst.name) -{ - setObjectName(QStringLiteral("ProvListItem")); - setFrameShape(QFrame::NoFrame); - setAutoFillBackground(true); - setCursor(Qt::PointingHandCursor); - - auto *headerRow = new QHBoxLayout; - headerRow->setContentsMargins(0, 0, 0, 0); - headerRow->setSpacing(6); - m_statusDot = new QLabel(QStringLiteral("●"), this); - QFont df = m_statusDot->font(); - df.setPixelSize(11); - m_statusDot->setFont(df); - m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(Status::Unknown))); - m_nameLabel = new QLabel(inst.name, this); - QFont nf = m_nameLabel->font(); - nf.setBold(true); - nf.setPixelSize(12); - m_nameLabel->setFont(nf); - headerRow->addWidget(m_statusDot, 0, Qt::AlignVCenter); - headerRow->addWidget(m_nameLabel, 1); - - m_urlLabel = new QLabel(inst.url, this); - m_urlLabel->setFont(monospaceFont(10)); - QPalette up = m_urlLabel->palette(); - up.setColor(QPalette::WindowText, up.color(QPalette::Mid)); - m_urlLabel->setPalette(up); - m_urlLabel->setContentsMargins(17, 0, 0, 0); - - auto *outer = new QVBoxLayout(this); - outer->setContentsMargins(8, 6, 8, 6); - outer->setSpacing(2); - outer->addLayout(headerRow); - outer->addWidget(m_urlLabel); - - applyTheme(); -} - -void ProviderListItem::setStatus(Status s) -{ - m_status = s; - m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(s))); -} - -void ProviderListItem::setSelected(bool s) -{ - if (m_selected == s) - return; - m_selected = s; - applyTheme(); -} - -void ProviderListItem::mouseReleaseEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - emit clicked(m_name); - QFrame::mouseReleaseEvent(event); -} - -void ProviderListItem::changeEvent(QEvent *event) -{ - QFrame::changeEvent(event); - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) - applyTheme(); -} - -QString ProviderListItem::statusColor(Status s) -{ - switch (s) { - case Status::Ok: return QStringLiteral("#3a8a4f"); - case Status::Fail: return QStringLiteral("#c94a4a"); - case Status::Unknown: return QStringLiteral("#888888"); - } - return QStringLiteral("#888888"); -} - -void ProviderListItem::applyTheme() -{ - if (m_inApplyTheme) - return; - QScopedValueRollback guard(m_inApplyTheme, true); - const Theme theme = themeFor(palette()); - setStyleSheet(QStringLiteral( - "#ProvListItem { background:%1; border-top: 1px solid %2; }") - .arg(m_selected ? theme.rowSelectedBg : QStringLiteral("transparent"), - theme.rowSeparator)); -} - -} // namespace QodeAssist::Settings diff --git a/settings/ProviderListItem.hpp b/settings/ProviderListItem.hpp deleted file mode 100644 index d7df85b..0000000 --- a/settings/ProviderListItem.hpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -class QLabel; -class QMouseEvent; - -namespace QodeAssist::Providers { -struct ProviderInstance; -} - -namespace QodeAssist::Settings { - -class ProviderListItem : public QFrame -{ - Q_OBJECT -public: - enum class Status : int { Unknown, Ok, Fail }; - - explicit ProviderListItem( - const Providers::ProviderInstance &inst, QWidget *parent = nullptr); - - void setStatus(Status s); - void setSelected(bool s); - QString providerName() const { return m_name; } - -signals: - void clicked(const QString &name); - -protected: - void mouseReleaseEvent(QMouseEvent *event) override; - void changeEvent(QEvent *event) override; - -private: - static QString statusColor(Status s); - void applyTheme(); - - QString m_name; - Status m_status = Status::Unknown; - bool m_selected = false; - bool m_inApplyTheme = false; - QLabel *m_statusDot = nullptr; - QLabel *m_nameLabel = nullptr; - QLabel *m_urlLabel = nullptr; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/ProviderSettings.cpp b/settings/ProviderSettings.cpp index 069ce3f..f7c050c 100644 --- a/settings/ProviderSettings.cpp +++ b/settings/ProviderSettings.cpp @@ -256,8 +256,6 @@ public: } }; -#ifndef QODEASSIST_EXPERIMENTAL const ProviderSettingsPage providerSettingsPage; -#endif } // namespace QodeAssist::Settings diff --git a/settings/ProvidersSettingsPage.cpp b/settings/ProvidersSettingsPage.cpp deleted file mode 100644 index e973244..0000000 --- a/settings/ProvidersSettingsPage.cpp +++ /dev/null @@ -1,618 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "ProvidersSettingsPage.hpp" - -#include -#include - -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ProviderDetailPane.hpp" -#include "ProviderInstance.hpp" -#include "ProviderInstanceFactory.hpp" -#include "ProviderInstanceWriter.hpp" -#include "ProviderLauncher.hpp" -#include "ProviderListItem.hpp" -#include "ProviderSecretsStore.hpp" -#include "SettingsConstants.hpp" -#include "SettingsTheme.hpp" - -namespace QodeAssist::Settings { - -ProvidersPageNavigator::ProvidersPageNavigator(QObject *parent) - : QObject(parent) -{} - -void ProvidersPageNavigator::requestSelectInstance(const QString &name) -{ - m_pending = name; - emit selectInstanceRequested(name); -} - -QString ProvidersPageNavigator::takePendingSelection() -{ - QString p = m_pending; - m_pending.clear(); - return p; -} - -namespace { - -class ProvidersPageWidget : public Core::IOptionsPageWidget -{ - Q_OBJECT -public: - ProvidersPageWidget( - Providers::ProviderInstanceFactory *factory, - Providers::ProviderSecretsStore *secrets, - Providers::ProviderLauncher *launcher, - ProvidersPageNavigator *navigator) - : m_factory(factory) - , m_secrets(secrets) - , m_launcher(launcher) - , m_navigator(navigator) - { - m_titleLabel = new QLabel(tr("Providers"), this); - QFont tf = m_titleLabel->font(); - tf.setBold(true); - tf.setPixelSize(13); - m_titleLabel->setFont(tf); - - auto *headerRow = new QHBoxLayout; - headerRow->setContentsMargins(0, 0, 0, 0); - headerRow->setSpacing(8); - headerRow->addWidget(m_titleLabel, 1); - - auto *headerSep = new QFrame(this); - headerSep->setFrameShape(QFrame::HLine); - headerSep->setFrameShadow(QFrame::Sunken); - - m_filterEdit = new QLineEdit(this); - m_filterEdit->setPlaceholderText(tr("Filter providers…")); - - m_listScroll = new QScrollArea(this); - m_listScroll->setWidgetResizable(true); - m_listScroll->setFrameShape(QFrame::NoFrame); - m_listContent = new QWidget(this); - m_listLayout = new QVBoxLayout(m_listContent); - m_listLayout->setContentsMargins(0, 0, 0, 0); - m_listLayout->setSpacing(0); - m_listLayout->addStretch(1); - m_listScroll->setWidget(m_listContent); - - auto *leftBox = new QFrame(this); - leftBox->setFrameShape(QFrame::StyledPanel); - auto *leftLay = new QVBoxLayout(leftBox); - leftLay->setContentsMargins(0, 0, 0, 0); - leftLay->setSpacing(0); - auto *filterRow = new QHBoxLayout; - filterRow->setContentsMargins(6, 6, 6, 6); - filterRow->addWidget(m_filterEdit, 1); - leftLay->addLayout(filterRow); - leftLay->addWidget(m_listScroll, 1); - - m_detailPane = new ProviderDetailPane(this); - connect(m_detailPane, &ProviderDetailPane::saveRequested, - this, &ProvidersPageWidget::onSaveEdited); - connect(m_detailPane, &ProviderDetailPane::duplicateRequested, - this, &ProvidersPageWidget::onDuplicateClicked); - connect(m_detailPane, &ProviderDetailPane::deleteRequested, - this, &ProvidersPageWidget::onRemoveClicked); - connect(m_detailPane, &ProviderDetailPane::apiKeySaveRequested, - this, &ProvidersPageWidget::onApiKeySave); - connect(m_detailPane, &ProviderDetailPane::apiKeyClearRequested, - this, &ProvidersPageWidget::onApiKeyClear); - connect(m_detailPane, &ProviderDetailPane::launchStartRequested, - this, &ProvidersPageWidget::onLaunchStart); - connect(m_detailPane, &ProviderDetailPane::launchStopRequested, - this, &ProvidersPageWidget::onLaunchStop); - connect(m_detailPane, &ProviderDetailPane::launchRestartRequested, - this, &ProvidersPageWidget::onLaunchRestart); - connect(m_detailPane, &ProviderDetailPane::openInEditorRequested, - this, [this](const QString &path) { - if (path.isEmpty() || path.startsWith(QLatin1String(":/"))) { - QMessageBox::information( - this, tr("Open in editor"), - tr("Bundled providers are read-only. " - "Use Duplicate to create an editable user copy first.")); - return; - } - Core::EditorManager::openEditor(Utils::FilePath::fromString(path)); - }); - if (m_launcher) { - connect(m_launcher.data(), &Providers::ProviderLauncher::stateChanged, - this, [this](const QString &name, - Providers::ProviderLauncher::State newState) { - if (name == m_currentName) - refreshDetailLaunch(); - const ProviderListItem::Status status = rowStatusFromState(newState); - for (auto *row : m_rows) { - if (row->providerName() == name) - row->setStatus(status); - } - }); - connect(m_launcher.data(), &Providers::ProviderLauncher::bytesReceived, - this, [this](const QString &name, const QByteArray &chunk) { - if (name == m_currentName) - m_detailPane->appendLaunchBytes(chunk); - }); - } - m_detailScroll = new QScrollArea(this); - m_detailScroll->setWidgetResizable(true); - m_detailScroll->setFrameShape(QFrame::StyledPanel); - m_detailScroll->setWidget(m_detailPane); - - auto *splitter = new QSplitter(Qt::Horizontal, this); - splitter->addWidget(leftBox); - splitter->addWidget(m_detailScroll); - splitter->setStretchFactor(0, 0); - splitter->setStretchFactor(1, 1); - splitter->setSizes({320, 700}); - - auto *root = new QVBoxLayout(this); - root->setContentsMargins(8, 8, 8, 8); - root->setSpacing(6); - root->addLayout(headerRow); - root->addWidget(headerSep); - root->addWidget(splitter, 1); - - m_filterDebounce = new QTimer(this); - m_filterDebounce->setSingleShot(true); - m_filterDebounce->setInterval(100); - connect(m_filterDebounce, &QTimer::timeout, this, - &ProvidersPageWidget::rebuildList); - connect(m_filterEdit, &QLineEdit::textChanged, this, - [this](const QString &) { m_filterDebounce->start(); }); - - if (m_factory) { - connect(m_factory.data(), - &Providers::ProviderInstanceFactory::instancesReloaded, - this, &ProvidersPageWidget::rebuildList); - } - if (m_navigator) { - connect(m_navigator.data(), - &ProvidersPageNavigator::selectInstanceRequested, - this, &ProvidersPageWidget::selectInstance); - } - - rebuildList(); - - const QString pending - = m_navigator ? m_navigator->takePendingSelection() : QString{}; - if (!pending.isEmpty()) - selectInstance(pending); - else if (m_factory && !m_factory->instances().empty()) - selectInstance(m_factory->instances().front().name); - } - - void apply() final {} - -private slots: - void rebuildList() - { - if (!m_factory) - return; - while (m_listLayout->count() > 0) { - QLayoutItem *item = m_listLayout->takeAt(0); - if (auto *w = item->widget()) - w->deleteLater(); - delete item; - } - m_rows.clear(); - m_listLayout->addStretch(1); // re-add trailing stretch - - const QString filter = m_filterEdit->text().trimmed().toLower(); - auto matches = [&](const Providers::ProviderInstance &inst) { - if (filter.isEmpty()) - return true; - return inst.name.toLower().contains(filter) - || inst.clientApi.toLower().contains(filter) - || inst.url.toLower().contains(filter); - }; - - - auto addSection = [&](const QString &title, bool userSection) { - auto *header = new QLabel(title.toUpper(), m_listContent); - QFont hf = header->font(); - hf.setPixelSize(10); - hf.setLetterSpacing(QFont::AbsoluteSpacing, 0.5); - header->setFont(hf); - QPalette hp = header->palette(); - hp.setColor(QPalette::WindowText, hp.color(QPalette::Mid)); - header->setPalette(hp); - header->setContentsMargins(8, 4, 8, 4); - header->setAutoFillBackground(true); - header->setStyleSheet( - QStringLiteral("QLabel { background:%1; }") - .arg(themeFor(palette()).listHeaderBg)); - m_listLayout->insertWidget(m_listLayout->count() - 1, header); - - std::vector sorted; - for (const auto &inst : m_factory->instances()) { - if (inst.isUserSource() != userSection) - continue; - if (!matches(inst)) - continue; - sorted.push_back(&inst); - } - std::sort(sorted.begin(), sorted.end(), - [](const Providers::ProviderInstance *a, - const Providers::ProviderInstance *b) { - return a->name.compare(b->name, Qt::CaseInsensitive) < 0; - }); - - int shown = 0; - for (const auto *inst : sorted) { - auto *row = new ProviderListItem(*inst, m_listContent); - connect(row, &ProviderListItem::clicked, - this, &ProvidersPageWidget::selectInstance); - if (m_launcher) - row->setStatus(rowStatusFromLauncher(inst->name)); - m_rows.append(row); - m_listLayout->insertWidget(m_listLayout->count() - 1, row); - ++shown; - } - if (shown == 0) { - auto *empty = new QLabel( - userSection ? tr("No user instances yet.") - : tr("No bundled instances loaded."), - m_listContent); - empty->setContentsMargins(10, 6, 10, 6); - QPalette ep = empty->palette(); - ep.setColor(QPalette::WindowText, ep.color(QPalette::Mid)); - empty->setPalette(ep); - m_listLayout->insertWidget(m_listLayout->count() - 1, empty); - } - }; - - addSection(tr("User"), true); - addSection(tr("Bundled"), false); - - for (auto *row : m_rows) - row->setSelected(row->providerName() == m_currentName); - - if (!m_currentName.isEmpty()) - populateDetail(m_currentName); - else - m_detailPane->clear(); - } - - void selectInstance(const QString &name) - { - if (name.isEmpty()) - return; - const auto *inst = m_factory ? m_factory->instanceByName(name) : nullptr; - if (!inst) - return; - m_currentName = inst->name; - for (auto *row : m_rows) - row->setSelected(row->providerName() == inst->name); - populateDetail(inst->name); - } - - void onDuplicateClicked() - { - if (!m_factory || m_currentName.isEmpty()) - return; - const Providers::ProviderInstance *srcPtr - = m_factory->instanceByName(m_currentName); - if (!srcPtr) - return; - const Providers::ProviderInstance srcCopy = *srcPtr; - bool ok = false; - const QString name = QInputDialog::getText( - this, tr("Duplicate provider"), - tr("Name for the new provider:"), QLineEdit::Normal, - QStringLiteral("%1 (copy)").arg(srcCopy.name), &ok); - if (!ok || name.trimmed().isEmpty()) - return; - if (m_factory->instanceByName(name.trimmed())) { - QMessageBox::warning(this, tr("Duplicate provider"), - tr("An instance named '%1' already exists.").arg(name.trimmed())); - return; - } - Providers::ProviderInstance copy = srcCopy; - copy.name = name.trimmed(); - copy.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(copy.name); - copy.sourcePath.clear(); - copy.overridesBundled = false; - QString writeErr; - if (Providers::ProviderInstanceWriter::writeToUserDir( - copy, /*previousPath=*/QString{}, &writeErr).isEmpty()) { - QMessageBox::warning(this, tr("Duplicate provider"), writeErr); - return; - } - m_factory->reload(); - selectInstance(copy.name); - } - - void onRemoveClicked() - { - if (!m_factory || m_currentName.isEmpty()) - return; - const Providers::ProviderInstance *instPtr - = m_factory->instanceByName(m_currentName); - if (!instPtr || !instPtr->isUserSource()) - return; - - const QString instName = instPtr->name; - const QString sourcePath = instPtr->sourcePath; - if (QMessageBox::question( - this, tr("Delete provider"), - tr("Delete user provider '%1'?\n\nFile: %2").arg(instName, sourcePath)) - != QMessageBox::Yes) - return; - if (!QFile::remove(sourcePath)) { - QMessageBox::warning(this, tr("Delete provider"), - tr("Failed to delete file:\n%1").arg(sourcePath)); - return; - } - m_currentName.clear(); - m_factory->reload(); - m_detailPane->clear(); - } - - void onSaveEdited(const Providers::ProviderInstance &edited) - { - if (!m_factory) - return; - Providers::ProviderInstance e = edited; - if (e.name.isEmpty()) { - QMessageBox::warning(this, tr("Save"), tr("Name cannot be empty.")); - return; - } - const auto *prior = m_factory->instanceByName(m_currentName); - const QString priorRef = prior ? prior->apiKeyRef : QString{}; - const QString priorName = prior ? prior->name : QString{}; - const bool nameChanged = !priorName.isEmpty() && priorName != e.name; - if (e.apiKeyRef.isEmpty() || (nameChanged && e.apiKeyRef == priorRef)) - e.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(e.name); - - const QString validation = Providers::ProviderInstance::validate( - e, m_factory->knownClientApis()); - if (!validation.isEmpty()) { - QMessageBox::warning(this, tr("Save"), validation); - return; - } - if (nameChanged) { - const auto *clash = m_factory->instanceByName(e.name); - if (clash) { - QMessageBox::warning(this, tr("Save"), - tr("An instance named '%1' already exists.").arg(e.name)); - return; - } - } - const QString softWarning = Providers::ProviderInstance::warnings(e); - if (!softWarning.isEmpty()) { - if (QMessageBox::warning(this, tr("Save"), - softWarning + QStringLiteral("\n\n") - + tr("Save anyway?"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No) - != QMessageBox::Yes) - return; - } - - const QString previousPath - = (prior && prior->isUserSource()) ? prior->sourcePath : QString{}; - QString writeErr; - const QString writtenPath = Providers::ProviderInstanceWriter::writeToUserDir( - e, previousPath, &writeErr); - if (writtenPath.isEmpty()) { - QMessageBox::warning(this, tr("Save"), writeErr); - return; - } - if (!previousPath.isEmpty() - && QFileInfo(writtenPath).absoluteFilePath() - != QFileInfo(previousPath).absoluteFilePath()) { - if (!QFile::remove(previousPath)) { - QMessageBox::warning( - this, tr("Save"), - tr("Saved to:\n%1\n\nbut could not remove the old file:\n%2\n\n" - "Two provider files now describe this instance — delete the " - "old file manually to avoid a duplicate-name error.") - .arg(writtenPath, previousPath)); - } - } - - if (m_secrets && !priorRef.isEmpty() && priorRef != e.apiKeyRef) { - const QString carried = m_secrets->readKeySync(priorRef); - if (!carried.isEmpty()) - m_secrets->writeKey(e.apiKeyRef, carried); - m_secrets->eraseKey(priorRef); - } - m_factory->reload(); - selectInstance(e.name); - } - - void onApiKeySave(const QString &newKey) - { - if (!m_factory || !m_secrets || m_currentName.isEmpty() || newKey.isEmpty()) - return; - const auto *inst = m_factory->instanceByName(m_currentName); - if (!inst || inst->apiKeyRef.isEmpty()) - return; - m_secrets->writeKey(inst->apiKeyRef, newKey); - m_detailPane->refreshKeyStatus(true); - } - - void onApiKeyClear() - { - if (!m_factory || !m_secrets || m_currentName.isEmpty()) - return; - const Providers::ProviderInstance *instPtr - = m_factory->instanceByName(m_currentName); - if (!instPtr || instPtr->apiKeyRef.isEmpty()) - return; - const QString instName = instPtr->name; - const QString apiKeyRef = instPtr->apiKeyRef; - if (QMessageBox::question( - this, tr("Clear API key"), - tr("Erase the stored API key for '%1'?").arg(instName)) - != QMessageBox::Yes) - return; - m_secrets->eraseKey(apiKeyRef); - m_detailPane->refreshKeyStatus(false); - } - - void onLaunchStart(const QString &name) - { - if (!m_factory || !m_launcher) - return; - const auto *inst = m_factory->instanceByName(name); - if (!inst || inst->launch.isEmpty()) - return; - m_launcher->start(name, inst->launch); - } - - void onLaunchStop(const QString &name) - { - if (!m_launcher) - return; - m_launcher->stop(name); - } - - void onLaunchRestart(const QString &name) - { - if (!m_factory || !m_launcher) - return; - const auto *inst = m_factory->instanceByName(name); - if (!inst || inst->launch.isEmpty()) - return; - m_launcher->restart(name, inst->launch); - } - -private: - void populateDetail(const QString &name) - { - if (!m_factory) - return; - const auto *inst = m_factory->instanceByName(name); - if (!inst) { - m_detailPane->clear(); - return; - } - const bool hasStoredKey - = m_secrets && !inst->apiKeyRef.isEmpty() && m_secrets->hasKey(inst->apiKeyRef); - m_detailPane->populate(*inst, hasStoredKey); - - if (m_launcher) { - m_detailPane->setLaunchState( - m_launcher->state(inst->name), - m_launcher->lastError(inst->name)); - m_detailPane->resetLaunchTerminal(m_launcher->scrollback(inst->name)); - } else { - m_detailPane->setLaunchState(Providers::ProviderLauncher::Idle, {}); - m_detailPane->resetLaunchTerminal({}); - } - } - - QPointer m_factory; - QPointer m_secrets; - QPointer m_navigator; - - QLabel *m_titleLabel = nullptr; - QLineEdit *m_filterEdit = nullptr; - - QScrollArea *m_listScroll = nullptr; - QWidget *m_listContent = nullptr; - QVBoxLayout *m_listLayout = nullptr; - QList m_rows; - - QScrollArea *m_detailScroll = nullptr; - ProviderDetailPane *m_detailPane = nullptr; - - QString m_currentName; - - QPointer m_launcher; - QTimer *m_filterDebounce = nullptr; - - void refreshDetailLaunch() - { - if (!m_launcher || m_currentName.isEmpty()) - return; - m_detailPane->setLaunchState( - m_launcher->state(m_currentName), - m_launcher->lastError(m_currentName)); - } - - static ProviderListItem::Status rowStatusFromState( - Providers::ProviderLauncher::State state) - { - switch (state) { - case Providers::ProviderLauncher::Ready: - return ProviderListItem::Status::Ok; - case Providers::ProviderLauncher::Failed: - return ProviderListItem::Status::Fail; - case Providers::ProviderLauncher::Idle: - case Providers::ProviderLauncher::Starting: - case Providers::ProviderLauncher::Probing: - case Providers::ProviderLauncher::Stopping: - return ProviderListItem::Status::Unknown; - } - return ProviderListItem::Status::Unknown; - } - - ProviderListItem::Status rowStatusFromLauncher(const QString &name) const - { - if (!m_launcher) - return ProviderListItem::Status::Unknown; - return rowStatusFromState(m_launcher->state(name)); - } -}; - -class ProvidersOptionsPage : public Core::IOptionsPage -{ -public: - ProvidersOptionsPage( - Providers::ProviderInstanceFactory *factory, - Providers::ProviderSecretsStore *secrets, - Providers::ProviderLauncher *launcher, - ProvidersPageNavigator *navigator) - { - setId(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID); - setDisplayName(QObject::tr("Providers")); - setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY); - setWidgetCreator([factory, secrets, launcher, navigator] { - return new ProvidersPageWidget(factory, secrets, launcher, navigator); - }); - } -}; - -} // namespace - -std::unique_ptr createProvidersSettingsPage( - Providers::ProviderInstanceFactory *instanceFactory, - Providers::ProviderSecretsStore *secrets, - Providers::ProviderLauncher *launcher, - ProvidersPageNavigator *navigator) -{ - return std::make_unique( - instanceFactory, secrets, launcher, navigator); -} - -} // namespace QodeAssist::Settings - -#include "ProvidersSettingsPage.moc" diff --git a/settings/ProvidersSettingsPage.hpp b/settings/ProvidersSettingsPage.hpp deleted file mode 100644 index 1d4b41f..0000000 --- a/settings/ProvidersSettingsPage.hpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include -#include - -namespace Core { class IOptionsPage; } - -namespace QodeAssist::Providers { -class ProviderInstanceFactory; -class ProviderSecretsStore; -class ProviderLauncher; -} - -namespace QodeAssist::Settings { - -class ProvidersPageNavigator : public QObject -{ - Q_OBJECT -public: - explicit ProvidersPageNavigator(QObject *parent = nullptr); - - void requestSelectInstance(const QString &name); - QString takePendingSelection(); - -signals: - void selectInstanceRequested(const QString &name); - -private: - QString m_pending; -}; - -std::unique_ptr createProvidersSettingsPage( - Providers::ProviderInstanceFactory *instanceFactory, - Providers::ProviderSecretsStore *secrets, - Providers::ProviderLauncher *launcher, - ProvidersPageNavigator *navigator); - -} // namespace QodeAssist::Settings diff --git a/settings/SectionBox.cpp b/settings/SectionBox.cpp deleted file mode 100644 index e55c42a..0000000 --- a/settings/SectionBox.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "SectionBox.hpp" - -#include -#include - -namespace QodeAssist::Settings { - -SectionBox::SectionBox(const QString &title, QWidget *parent) - : QWidget(parent) -{ - m_title = new QLabel(title, this); - QFont tf = m_title->font(); - tf.setBold(true); - m_title->setFont(tf); - - m_body = new QWidget(this); - m_bodyLayout = new QVBoxLayout(m_body); - m_bodyLayout->setContentsMargins(0, 0, 0, 0); - m_bodyLayout->setSpacing(4); - - auto *outer = new QVBoxLayout(this); - outer->setContentsMargins(0, 4, 0, 4); - outer->setSpacing(4); - outer->addWidget(m_title); - outer->addWidget(m_body, 1); -} - -} // namespace QodeAssist::Settings diff --git a/settings/SectionBox.hpp b/settings/SectionBox.hpp deleted file mode 100644 index 3bdeede..0000000 --- a/settings/SectionBox.hpp +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -class QLabel; -class QVBoxLayout; - -namespace QodeAssist::Settings { - -class SectionBox : public QWidget -{ -public: - explicit SectionBox(const QString &title, QWidget *parent = nullptr); - - QVBoxLayout *bodyLayout() const { return m_bodyLayout; } - -private: - QLabel *m_title = nullptr; - QWidget *m_body = nullptr; - QVBoxLayout *m_bodyLayout = nullptr; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 57f2c97..4770877 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -140,12 +140,6 @@ const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "QodeAssist"; // Provider Settings Page ID const char QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID[] = "QodeAssist.7ProviderSettingsPageId"; -// Agents Settings Page ID -const char QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID[] = "QodeAssist.8AgentsSettingsPageId"; - -// Agent Pipelines (experimental) settings -const char QODE_ASSIST_AGENT_PIPELINES_PAGE_ID[] = "QodeAssist.9AgentPipelinesPageId"; - // Provider API Keys const char OPEN_ROUTER_API_KEY[] = "QodeAssist.openRouterApiKey"; const char OPEN_ROUTER_API_KEY_HISTORY[] = "QodeAssist.openRouterApiKeyHistory"; diff --git a/settings/SettingsTheme.hpp b/settings/SettingsTheme.hpp deleted file mode 100644 index 4340de3..0000000 --- a/settings/SettingsTheme.hpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -struct Theme -{ - bool dark = false; - QString listHeaderBg; - QString rowSeparator; - QString rowSelectedBg; - QString codeBg; -}; - -inline bool isDarkPalette(const QPalette &p) -{ - return p.color(QPalette::Window).lightness() < 128; -} - -inline Theme themeFor(const QPalette &p) -{ - const bool dark = isDarkPalette(p); - if (dark) - return {true, - QStringLiteral("#262626"), - QStringLiteral("#3a3a3a"), - QStringLiteral("#2c4060"), - QStringLiteral("#1f1f1f")}; - return {false, - QStringLiteral("#f0f0f0"), - QStringLiteral("#dcdcdc"), - QStringLiteral("#cfe2ff"), - QStringLiteral("#f4f4f4")}; -} - -inline QFont monospaceFont(int pixelSize = 11) -{ - QFont f = QFontDatabase::systemFont(QFontDatabase::FixedFont); - f.setStyleHint(QFont::Monospace); - if (pixelSize > 0) - f.setPixelSize(pixelSize); - return f; -} - -} // namespace QodeAssist::Settings diff --git a/settings/SettingsUiBuilders.cpp b/settings/SettingsUiBuilders.cpp deleted file mode 100644 index 717757c..0000000 --- a/settings/SettingsUiBuilders.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "SettingsUiBuilders.hpp" - -#include "SettingsTheme.hpp" - -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -void applyMutedSmallCaps(QLabel *label) -{ - QFont f = label->font(); - f.setPixelSize(10); - f.setLetterSpacing(QFont::AbsoluteSpacing, 0.4); - label->setFont(f); - QPalette p = label->palette(); - p.setColor(QPalette::WindowText, p.color(QPalette::Mid)); - label->setPalette(p); -} - -QLabel *makeSectionHeader(const QString &title, QWidget *parent) -{ - auto *header = new QLabel(title.toUpper(), parent); - applyMutedSmallCaps(header); - header->setContentsMargins(8, 4, 8, 4); - header->setAutoFillBackground(true); - const Theme theme = themeFor(parent ? parent->palette() : QPalette()); - header->setStyleSheet( - QStringLiteral("QLabel { background:%1; border-top:1px solid %2;" - " border-bottom:1px solid %2; }") - .arg(theme.listHeaderBg, theme.rowSeparator)); - return header; -} - -QLabel *makeHintLabel(const QString &text, QWidget *parent) -{ - auto *h = new QLabel(text, parent); - QFont hf = h->font(); - hf.setPixelSize(11); - h->setFont(hf); - h->setWordWrap(true); - QPalette p = h->palette(); - p.setColor(QPalette::WindowText, p.color(QPalette::Mid)); - h->setPalette(p); - return h; -} - -QHBoxLayout *singleField(QWidget *w) -{ - auto *lay = new QHBoxLayout; - lay->setContentsMargins(0, 0, 0, 0); - lay->setSpacing(4); - lay->addWidget(w, 1); - return lay; -} - -namespace { - -QLabel *makeFormLabel(const QString &text) -{ - auto *l = new QLabel(text); - l->setMinimumWidth(96); - l->setAlignment(Qt::AlignLeft | Qt::AlignTop); - return l; -} - -} // namespace - -FormBuilder::FormBuilder(QGridLayout *grid, int startRow) - : m_grid(grid) - , m_row(startRow) -{} - -FormBuilder &FormBuilder::row(const QString &label, QLayout *value, const QString &hint) -{ - m_grid->addWidget(makeFormLabel(label), m_row, 0, Qt::AlignTop); - auto *holder = new QWidget; - holder->setLayout(value); - m_grid->addWidget(holder, m_row, 1); - ++m_row; - if (!hint.isEmpty()) { - m_grid->addWidget(makeHintLabel(hint), m_row, 1); - ++m_row; - } - return *this; -} - -FormBuilder &FormBuilder::row(const QString &label, QWidget *value, const QString &hint) -{ - return row(label, singleField(value), hint); -} - -} // namespace QodeAssist::Settings diff --git a/settings/SettingsUiBuilders.hpp b/settings/SettingsUiBuilders.hpp deleted file mode 100644 index 286b4bf..0000000 --- a/settings/SettingsUiBuilders.hpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -class QGridLayout; -class QHBoxLayout; -class QLabel; -class QLayout; -class QWidget; - -namespace QodeAssist::Settings { - -void applyMutedSmallCaps(QLabel *label); - -QLabel *makeSectionHeader(const QString &title, QWidget *parent); - -QLabel *makeHintLabel(const QString &text, QWidget *parent = nullptr); - -QHBoxLayout *singleField(QWidget *w); - -class FormBuilder -{ -public: - explicit FormBuilder(QGridLayout *grid, int startRow = 0); - - FormBuilder &row(const QString &label, QLayout *value, const QString &hint = {}); - FormBuilder &row(const QString &label, QWidget *value, const QString &hint = {}); - - int currentRow() const { return m_row; } - -private: - QGridLayout *m_grid; - int m_row; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/TagChip.cpp b/settings/TagChip.cpp deleted file mode 100644 index dda8ed4..0000000 --- a/settings/TagChip.cpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TagChip.hpp" - -#include "SettingsTheme.hpp" - -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -TagChip::TagChip(const QString &tag, int count, QWidget *parent) - : QFrame(parent) - , m_tag(tag) -{ - setObjectName(QStringLiteral("TagChip")); - setCursor(Qt::PointingHandCursor); - - m_label = new QLabel(tag, this); - m_label->setFont(monospaceFont(11)); - - auto *row = new QHBoxLayout(this); - row->setContentsMargins(5, 0, 5, 0); - row->setSpacing(4); - row->addWidget(m_label); - - if (count >= 0) { - m_count = new QLabel(QString::number(count), this); - QFont cf = m_count->font(); - cf.setPixelSize(10); - m_count->setFont(cf); - row->addWidget(m_count); - } - applyTheme(); -} - -void TagChip::setActive(bool on) -{ - if (m_active == on) - return; - m_active = on; - applyTheme(); -} - -void TagChip::mouseReleaseEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton) - emit clicked(m_tag); - QFrame::mouseReleaseEvent(event); -} - -void TagChip::changeEvent(QEvent *event) -{ - QFrame::changeEvent(event); - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) - applyTheme(); -} - -void TagChip::applyTheme() -{ - if (m_inApplyTheme) - return; - QScopedValueRollback guard(m_inApplyTheme, true); - const Theme theme = themeFor(palette()); - const QString text = palette().color(QPalette::WindowText).name(); - const QString mute = palette().color(QPalette::Mid).name(); - const QString border = m_active ? text : theme.rowSeparator; - const QString bg = m_active ? theme.rowSelectedBg : QStringLiteral("transparent"); - setStyleSheet(QStringLiteral( - "#TagChip { background:%1; border:1px solid %2; }") - .arg(bg, border)); - QPalette lp = m_label->palette(); - lp.setColor(QPalette::WindowText, m_active ? QColor(text) : QColor(mute)); - m_label->setPalette(lp); - if (m_count) { - QPalette cp = m_count->palette(); - cp.setColor(QPalette::WindowText, QColor(mute)); - m_count->setPalette(cp); - } -} - -} // namespace QodeAssist::Settings diff --git a/settings/TagChip.hpp b/settings/TagChip.hpp deleted file mode 100644 index 94a728b..0000000 --- a/settings/TagChip.hpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -class QLabel; - -namespace QodeAssist::Settings { - -class TagChip : public QFrame -{ - Q_OBJECT -public: - explicit TagChip(const QString &tag, int count, QWidget *parent = nullptr); - - void setActive(bool on); - QString tag() const { return m_tag; } - -signals: - void clicked(const QString &tag); - -protected: - void mouseReleaseEvent(QMouseEvent *event) override; - void changeEvent(QEvent *event) override; - -private: - void applyTheme(); - - QString m_tag; - bool m_active = false; - bool m_inApplyTheme = false; - QLabel *m_label = nullptr; - QLabel *m_count = nullptr; -}; - -} // namespace QodeAssist::Settings diff --git a/settings/TagFilterStrip.cpp b/settings/TagFilterStrip.cpp deleted file mode 100644 index 647ed6b..0000000 --- a/settings/TagFilterStrip.cpp +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "TagFilterStrip.hpp" - -#include "SettingsTheme.hpp" -#include "SettingsUiBuilders.hpp" -#include "TagChip.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::Settings { - -TagFilterStrip::TagFilterStrip(QWidget *parent) - : QWidget(parent) -{ - setObjectName(QStringLiteral("TagStrip")); - setAutoFillBackground(true); - m_layout = new QVBoxLayout(this); - m_layout->setContentsMargins(8, 6, 8, 6); - m_layout->setSpacing(5); - applyTheme(); -} - -void TagFilterStrip::setAvailableTags(const QMap &countsByTag) -{ - m_counts = countsByTag; - QSet stillExisting; - for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it) - stillExisting.insert(it.key()); - QSet trimmed; - for (const QString &t : m_activeTags) - if (stillExisting.contains(t)) - trimmed.insert(t); - const bool activeChanged = trimmed != m_activeTags; - if (activeChanged) - m_activeTags = trimmed; - rebuild(); - if (activeChanged) - emit activeTagsChanged(m_activeTags); -} - -void TagFilterStrip::changeEvent(QEvent *event) -{ - QWidget::changeEvent(event); - if (m_inApplyTheme) - return; - if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) - applyTheme(); -} - -void TagFilterStrip::toggleTag(const QString &tag) -{ - if (m_activeTags.contains(tag)) - m_activeTags.remove(tag); - else - m_activeTags.insert(tag); - refreshActiveStates(); - emit activeTagsChanged(m_activeTags); -} - -void TagFilterStrip::refreshActiveStates() -{ - for (auto it = m_chipByTag.cbegin(); it != m_chipByTag.cend(); ++it) - it.value()->setActive(m_activeTags.contains(it.key())); -} - -void TagFilterStrip::applyTheme() -{ - if (m_inApplyTheme) - return; - QScopedValueRollback guard(m_inApplyTheme, true); - const Theme theme = themeFor(palette()); - setStyleSheet(QStringLiteral("QWidget#TagStrip { background:%1;" - " border-bottom:1px solid %2; }") - .arg(theme.listHeaderBg, theme.rowSeparator)); -} - -void TagFilterStrip::rebuild() -{ - while (auto *item = m_layout->takeAt(0)) { - if (auto *w = item->widget()) - w->deleteLater(); - if (auto *l = item->layout()) { - while (auto *sub = l->takeAt(0)) { - if (auto *sw = sub->widget()) - sw->deleteLater(); - delete sub; - } - } - delete item; - } - m_chipByTag.clear(); - - if (m_counts.isEmpty()) { - setVisible(false); - return; - } - setVisible(true); - - auto *headerLine = new QHBoxLayout; - headerLine->setContentsMargins(0, 0, 0, 0); - headerLine->setSpacing(6); - auto *title = new QLabel(tr("FILTER BY TAG"), this); - applyMutedSmallCaps(title); - headerLine->addWidget(title); - headerLine->addStretch(1); - if (!m_activeTags.isEmpty()) { - auto *clear = new QLabel(QStringLiteral("%1").arg(tr("clear")), this); - connect(clear, &QLabel::linkActivated, this, [this](const QString &) { - if (m_activeTags.isEmpty()) - return; - m_activeTags.clear(); - refreshActiveStates(); - emit activeTagsChanged(m_activeTags); - }); - headerLine->addWidget(clear); - } - m_layout->addLayout(headerLine); - - std::vector> sorted; - sorted.reserve(m_counts.size()); - for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it) - sorted.emplace_back(it.key(), it.value()); - std::sort(sorted.begin(), sorted.end(), - [](const auto &a, const auto &b) { - if (a.second != b.second) - return a.second > b.second; - return a.first.localeAwareCompare(b.first) < 0; - }); - - auto *grid = new QGridLayout; - grid->setContentsMargins(0, 0, 0, 0); - grid->setHorizontalSpacing(3); - grid->setVerticalSpacing(3); - int col = 0, gridRow = 0; - for (const auto &[tag, count] : sorted) { - auto *chip = new TagChip(tag, count, this); - chip->setActive(m_activeTags.contains(tag)); - connect(chip, &TagChip::clicked, this, &TagFilterStrip::toggleTag); - grid->addWidget(chip, gridRow, col, Qt::AlignLeft); - m_chipByTag.insert(tag, chip); - if (++col >= 4) { - col = 0; - ++gridRow; - } - } - grid->setColumnStretch(4, 1); - m_layout->addLayout(grid); -} - -} // namespace QodeAssist::Settings diff --git a/settings/TagFilterStrip.hpp b/settings/TagFilterStrip.hpp deleted file mode 100644 index 8ca75fc..0000000 --- a/settings/TagFilterStrip.hpp +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include -#include - -class QVBoxLayout; - -namespace QodeAssist::Settings { - -class TagChip; - -class TagFilterStrip : public QWidget -{ - Q_OBJECT -public: - explicit TagFilterStrip(QWidget *parent = nullptr); - - void setAvailableTags(const QMap &countsByTag); - const QSet &activeTags() const { return m_activeTags; } - -signals: - void activeTagsChanged(const QSet &tags); - -protected: - void changeEvent(QEvent *event) override; - -private: - void rebuild(); - void refreshActiveStates(); - void applyTheme(); - void toggleTag(const QString &tag); - - QMap m_counts; - QSet m_activeTags; - QVBoxLayout *m_layout = nullptr; - QHash m_chipByTag; - bool m_inApplyTheme = false; -}; - -} // namespace QodeAssist::Settings diff --git a/sources/CMakeLists.txt b/sources/CMakeLists.txt index 9139c6c..13bade1 100644 --- a/sources/CMakeLists.txt +++ b/sources/CMakeLists.txt @@ -1,12 +1,2 @@ add_subdirectory(external) -add_subdirectory(tomlSerializer) add_subdirectory(skills) -add_subdirectory(common) -add_subdirectory(providers) -add_subdirectory(templates) -add_subdirectory(agents) -add_subdirectory(providersConfig) - -if(QODEASSIST_EXPERIMENTAL) - add_subdirectory(settings) -endif() diff --git a/sources/agents/Agent.cpp b/sources/agents/Agent.cpp deleted file mode 100644 index dc062a8..0000000 --- a/sources/agents/Agent.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "Agent.hpp" - -#include - -#include "JsonPromptTemplate.hpp" -#include "PromptTemplate.hpp" -#include "Provider.hpp" - -namespace QodeAssist { - -using Providers::Provider; -using Templates::JsonPromptTemplate; -using Templates::PromptTemplate; - -QString AgentConfig::validate(const AgentConfig &config) -{ - if (config.name.isEmpty()) - return QStringLiteral("Agent config has no name"); - if (config.schemaVersion > AgentConfig::kSupportedSchemaVersion) { - return QStringLiteral( - "Agent config '%1' declares schema_version %2 but this plugin " - "supports at most %3 — update QodeAssist to use this profile") - .arg(config.name) - .arg(config.schemaVersion) - .arg(AgentConfig::kSupportedSchemaVersion); - } - if (config.providerInstance.isEmpty()) - return QStringLiteral("Agent config '%1' has no provider_instance").arg(config.name); - if (config.model.isEmpty()) - return QStringLiteral("Agent config '%1' has no model").arg(config.name); - if (config.endpoint.isEmpty()) - return QStringLiteral("Agent config '%1' has no endpoint").arg(config.name); - if (config.messageFormat.isEmpty()) { - return QStringLiteral("Agent config '%1' has no [template].message_format") - .arg(config.name); - } - return {}; -} - -Agent::Agent(AgentConfig config, Providers::Provider *providerOwned, QObject *parent) - : QObject(parent) - , m_config(std::move(config)) - , m_provider(providerOwned) -{ - m_invalidReason = AgentConfig::validate(m_config); - if (!m_invalidReason.isEmpty()) - return; - - if (!m_provider) { - m_invalidReason - = QStringLiteral("Agent '%1' was constructed without a provider").arg(m_config.name); - return; - } - m_provider->setParent(this); - - QString tmplErr; - m_promptTemplate = JsonPromptTemplate::fromConfig(m_config, &tmplErr); - if (!m_promptTemplate) { - m_invalidReason = tmplErr.isEmpty() - ? QStringLiteral("Failed to build prompt template for agent '%1'") - .arg(m_config.name) - : tmplErr; - } -} - -Agent::~Agent() = default; - -PromptTemplate *Agent::promptTemplate() noexcept -{ - return m_promptTemplate.get(); -} - -const PromptTemplate *Agent::promptTemplate() const noexcept -{ - return m_promptTemplate.get(); -} - -QFuture> Agent::installedModels() -{ - Q_ASSERT_X(thread() == QThread::currentThread(), Q_FUNC_INFO, - "Agent::installedModels called from non-owning thread; " - "the underlying BaseClient is not thread-safe and must be " - "accessed from the Agent's owner thread"); - - if (!m_provider) { - return QtFuture::makeReadyValueFuture(QList{}); - } - return m_provider->getInstalledModels(m_provider->url()); -} - -} // namespace QodeAssist diff --git a/sources/agents/Agent.hpp b/sources/agents/Agent.hpp deleted file mode 100644 index d098358..0000000 --- a/sources/agents/Agent.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include -#include -#include -#include - -#include "AgentConfig.hpp" - -namespace QodeAssist { - -namespace Providers { -class Provider; -} -namespace Templates { -class JsonPromptTemplate; -class PromptTemplate; -} - -class Agent : public QObject -{ - Q_OBJECT - Q_DISABLE_COPY_MOVE(Agent) -public: - Agent(AgentConfig config, Providers::Provider *providerOwned, QObject *parent = nullptr); - ~Agent() override; - - const AgentConfig &config() const noexcept { return m_config; } - - Providers::Provider *provider() noexcept { return m_provider; } - const Providers::Provider *provider() const noexcept { return m_provider; } - - Templates::PromptTemplate *promptTemplate() noexcept; - const Templates::PromptTemplate *promptTemplate() const noexcept; - - bool isValid() const noexcept { return m_invalidReason.isEmpty(); } - QString invalidReason() const { return m_invalidReason; } - - QFuture> installedModels(); - -private: - AgentConfig m_config; - std::unique_ptr m_promptTemplate; // owned - Providers::Provider *m_provider = nullptr; // child of this - QString m_invalidReason; -}; - -} // namespace QodeAssist diff --git a/sources/agents/AgentConfig.hpp b/sources/agents/AgentConfig.hpp deleted file mode 100644 index 5d97700..0000000 --- a/sources/agents/AgentConfig.hpp +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include - -namespace QodeAssist { - -struct AgentConfig -{ - static constexpr int kSupportedSchemaVersion = 1; - int schemaVersion = 1; - QString name; - QString description; - QString providerInstance; - QString model; - QString endpoint; - QString role; - QStringList tags; - - struct Match - { - QStringList filePatterns; - QStringList pathPatterns; - QStringList projectNames; - - [[nodiscard]] bool isEmpty() const noexcept - { - return filePatterns.isEmpty() - && pathPatterns.isEmpty() - && projectNames.isEmpty(); - } - }; - Match match; - - bool enableThinking = false; - bool enableTools = false; - - QString messageFormat; - QJsonObject sampling; - QJsonObject thinking; - QString context; - QString extendsName; - bool abstract = false; - bool hidden = false; - - QString sourcePath; - bool overridesBundled = false; - bool isUserSource() const { return !sourcePath.startsWith(QLatin1StringView{":/"}); } - - static QString validate(const AgentConfig &config); -}; - -} // namespace QodeAssist diff --git a/sources/agents/AgentFactory.cpp b/sources/agents/AgentFactory.cpp deleted file mode 100644 index 9f88582..0000000 --- a/sources/agents/AgentFactory.cpp +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentFactory.hpp" - -#include -#include - -#include - -#include "Agent.hpp" -#include "AgentLoader.hpp" -#include "Provider.hpp" -#include "ProviderFactory.hpp" -#include "Logger.hpp" -#include "ProviderSecretsStore.hpp" -#include "ProviderInstance.hpp" -#include "ProviderInstanceFactory.hpp" - -static inline void initAgentsResource() { Q_INIT_RESOURCE(agents); } - -namespace { -Q_LOGGING_CATEGORY(agentFactoryLog, "qodeassist.agentfactory") - -QString agentQrcPrefix() { return QStringLiteral(":/agents"); } -} // namespace - -namespace QodeAssist { - -AgentFactory::AgentFactory( - Providers::ProviderInstanceFactory *instanceFactory, - Providers::ProviderSecretsStore *secrets, - QObject *parent) - : QObject(parent) - , m_instanceFactory(instanceFactory) - , m_secrets(secrets) -{ - ::initAgentsResource(); - reload(); -} - -AgentFactory::~AgentFactory() = default; - -QString AgentFactory::userAgentsDir() -{ - return Core::ICore::userResourcePath(QStringLiteral("qodeassist/config/agents")) - .toFSPathString(); -} - -void AgentFactory::reload() -{ - Q_ASSERT(thread() == QThread::currentThread()); - clear(); - - auto result = Agents::AgentLoader::load(agentQrcPrefix(), userAgentsDir()); - for (const QString &err : result.errors) - LOG_MESSAGE(QString("[Agents] error: %1").arg(err)); - for (const QString &warn : result.warnings) - LOG_MESSAGE(QString("[Agents] warning: %1").arg(warn)); - LOG_MESSAGE(QString("[Agents] Loaded %1 profiles (qrc=%2, user=%3)") - .arg(result.configs.size()) - .arg(agentQrcPrefix(), userAgentsDir())); - - for (auto &cfg : result.configs) { - LOG_MESSAGE(QString("[Agents] Loaded: %1").arg(cfg.name)); - registerConfig(std::move(cfg)); - } - m_errors = std::move(result.errors); - m_warnings = std::move(result.warnings); -} - -void AgentFactory::registerConfig(AgentConfig config) -{ - Q_ASSERT(thread() == QThread::currentThread()); - - const QString error = AgentConfig::validate(config); - if (!error.isEmpty()) { - qCWarning(agentFactoryLog).noquote() << "Rejected agent config:" << error; - return; - } - const auto it = m_indexByName.constFind(config.name); - if (it != m_indexByName.constEnd()) { - m_configs[it.value()] = std::move(config); - return; - } - m_indexByName.insert(config.name, static_cast(m_configs.size())); - m_configs.push_back(std::move(config)); -} - -const AgentConfig *AgentFactory::configByName(const QString &name) const -{ - const auto it = m_indexByName.constFind(name); - if (it == m_indexByName.constEnd()) - return nullptr; - return &m_configs[it.value()]; -} - -QStringList AgentFactory::configNames() const -{ - QStringList out; - out.reserve(static_cast(m_configs.size())); - for (const auto &c : m_configs) { - if (c.hidden) continue; - out.append(c.name); - } - return out; -} - -namespace { - -Providers::Provider *buildProviderForAgent( - const AgentConfig &cfg, - Providers::ProviderInstanceFactory *instanceFactory, - Providers::ProviderSecretsStore *secrets, - QString *errorOut) -{ - if (!instanceFactory) { - if (errorOut) { - *errorOut = QStringLiteral( - "Agent '%1' cannot be built — no ProviderInstanceFactory was wired " - "into AgentFactory") - .arg(cfg.name); - } - return nullptr; - } - const Providers::ProviderInstance *inst - = instanceFactory->instanceByName(cfg.providerInstance); - if (!inst) { - if (errorOut) { - *errorOut = QStringLiteral( - "Agent '%1' references unknown provider instance '%2'") - .arg(cfg.name, cfg.providerInstance); - } - return nullptr; - } - const QString validation = Providers::ProviderInstance::validate( - *inst, Providers::ProviderFactory::knownNames()); - if (!validation.isEmpty()) { - if (errorOut) - *errorOut = validation; - return nullptr; - } - Providers::Provider *provider = Providers::ProviderFactory::create(inst->clientApi, nullptr); - if (!provider) { - if (errorOut) { - *errorOut = QStringLiteral("Client API '%1' is not registered (instance '%2')") - .arg(inst->clientApi, inst->name); - } - return nullptr; - } - provider->setUrl(inst->url); - if (secrets && !inst->apiKeyRef.isEmpty()) - provider->setApiKey(secrets->readKeySync(inst->apiKeyRef)); - return provider; -} - -} // namespace - -Agent *AgentFactory::create(const QString &name, QObject *parent, QString *errorOut) const -{ - const AgentConfig *cfg = configByName(name); - if (!cfg) { - if (errorOut) - *errorOut = QStringLiteral("Agent '%1' is not registered").arg(name); - return nullptr; - } - Providers::Provider *provider = buildProviderForAgent( - *cfg, m_instanceFactory.data(), m_secrets.data(), errorOut); - if (!provider) - return nullptr; - auto agent = std::make_unique(*cfg, provider, /*parent=*/nullptr); - if (!agent->isValid()) { - if (errorOut) - *errorOut = agent->invalidReason(); - return nullptr; - } - agent->setParent(parent); - return agent.release(); -} - -Agent *AgentFactory::createFromFile( - const QString &tomlPath, QObject *parent, QString *errorOut) const -{ - QString parseErr; - QStringList warnings; - auto cfgOpt = Agents::AgentLoader::parseFile(tomlPath, &parseErr, &warnings); - if (!cfgOpt) { - if (errorOut) *errorOut = parseErr; - return nullptr; - } - Providers::Provider *provider = buildProviderForAgent( - *cfgOpt, m_instanceFactory.data(), m_secrets.data(), errorOut); - if (!provider) - return nullptr; - auto agent = std::make_unique(std::move(*cfgOpt), provider, /*parent=*/nullptr); - if (!agent->isValid()) { - if (errorOut) *errorOut = agent->invalidReason(); - return nullptr; - } - agent->setParent(parent); - return agent.release(); -} - -void AgentFactory::clear() -{ - Q_ASSERT(thread() == QThread::currentThread()); - m_configs.clear(); - m_indexByName.clear(); - m_errors.clear(); - m_warnings.clear(); -} - -Providers::ProviderInstanceFactory *AgentFactory::instanceFactory() const noexcept -{ - return m_instanceFactory.data(); -} - -Providers::ProviderSecretsStore *AgentFactory::secretsStore() const noexcept -{ - return m_secrets.data(); -} - -} // namespace QodeAssist diff --git a/sources/agents/AgentFactory.hpp b/sources/agents/AgentFactory.hpp deleted file mode 100644 index 670c133..0000000 --- a/sources/agents/AgentFactory.hpp +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "AgentConfig.hpp" - -namespace QodeAssist { - -class Agent; - -namespace Providers { -class ProviderInstanceFactory; -class ProviderSecretsStore; -} - -class AgentFactory : public QObject -{ - Q_OBJECT - Q_DISABLE_COPY_MOVE(AgentFactory) -public: - explicit AgentFactory( - Providers::ProviderInstanceFactory *instanceFactory = nullptr, - Providers::ProviderSecretsStore *secrets = nullptr, - QObject *parent = nullptr); - ~AgentFactory() override; - - void reload(); - - [[nodiscard]] static QString userAgentsDir(); - - [[nodiscard]] const AgentConfig *configByName(const QString &name) const; - [[nodiscard]] QStringList configNames() const; - [[nodiscard]] const std::vector &configs() const noexcept { return m_configs; } - - Agent *create(const QString &name, QObject *parent, QString *errorOut = nullptr) const; - - Agent *createFromFile( - const QString &tomlPath, QObject *parent, QString *errorOut = nullptr) const; - - [[nodiscard]] QStringList lastLoadErrors() const { return m_errors; } - [[nodiscard]] QStringList lastLoadWarnings() const { return m_warnings; } - - void registerConfig(AgentConfig config); - void clear(); - - [[nodiscard]] Providers::ProviderInstanceFactory *instanceFactory() const noexcept; - [[nodiscard]] Providers::ProviderSecretsStore *secretsStore() const noexcept; - -private: - std::vector m_configs; - QHash m_indexByName; - QStringList m_errors; - QStringList m_warnings; - QPointer m_instanceFactory; - QPointer m_secrets; -}; - -} // namespace QodeAssist diff --git a/sources/agents/AgentLoader.cpp b/sources/agents/AgentLoader.cpp deleted file mode 100644 index e62a722..0000000 --- a/sources/agents/AgentLoader.cpp +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentLoader.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include - -namespace QodeAssist::Agents { - -namespace { - -QJsonValue tomlToJson(const toml::node &node) -{ - if (auto *table = node.as_table()) { - QJsonObject obj; - for (const auto &[key, value] : *table) { - obj.insert(QString::fromStdString(std::string{key.str()}), tomlToJson(value)); - } - return obj; - } - if (auto *array = node.as_array()) { - QJsonArray arr; - for (const auto &item : *array) { - arr.append(tomlToJson(item)); - } - return arr; - } - if (auto *str = node.as_string()) { - return QString::fromStdString(str->get()); - } - if (auto *integer = node.as_integer()) { - return static_cast(integer->get()); - } - if (auto *floating = node.as_floating_point()) { - return floating->get(); - } - if (auto *boolean = node.as_boolean()) { - return boolean->get(); - } - return QJsonValue::Null; -} - -QJsonObject deepMerge(const QJsonObject &base, const QJsonObject &overlay) -{ - QJsonObject result = base; - for (auto it = overlay.constBegin(); it != overlay.constEnd(); ++it) { - const QJsonValue baseVal = result.value(it.key()); - const QJsonValue overlayVal = it.value(); - if (baseVal.isObject() && overlayVal.isObject()) { - result[it.key()] = deepMerge(baseVal.toObject(), overlayVal.toObject()); - } else { - result[it.key()] = overlayVal; - } - } - return result; -} - -QString readUtf8(const QString &path, QString *error) -{ - QFile f(path); - if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { - if (error) *error = QStringLiteral("Cannot open: %1").arg(path); - return {}; - } - return QString::fromUtf8(f.readAll()); -} - -std::optional parseTomlFile(const QString &path, QString *error) -{ - QString readErr; - const QString contents = readUtf8(path, &readErr); - if (!readErr.isEmpty()) { - if (error) *error = readErr; - return std::nullopt; - } - toml::table tbl; - try { - tbl = toml::parse(contents.toStdString(), path.toStdString()); - } catch (const toml::parse_error &e) { - std::ostringstream oss; - oss << e; - if (error) { - *error = QStringLiteral("TOML parse error in %1: %2") - .arg(path, QString::fromStdString(oss.str())); - } - return std::nullopt; - } - return tomlToJson(tbl).toObject(); -} - -QStringList stringArray(const QJsonValue &v) -{ - QStringList out; - if (!v.isArray()) return out; - for (const auto &elem : v.toArray()) { - if (elem.isString()) out.append(elem.toString()); - } - return out; -} - -AgentConfig configFromMerged(const QJsonObject &obj) -{ - AgentConfig cfg; - cfg.schemaVersion = obj.value("schema_version").toInt(1); - cfg.name = obj.value("name").toString(); - cfg.description = obj.value("description").toString(); - cfg.providerInstance = obj.value("provider_instance").toString(); - cfg.model = obj.value("model").toString(); - cfg.endpoint = obj.value("endpoint").toString(); - cfg.role = obj.value("role").toString(); - cfg.context = obj.value("context").toString(); - cfg.enableThinking = obj.value("enable_thinking").toBool(false); - cfg.enableTools = obj.value("enable_tools").toBool(false); - cfg.tags = stringArray(obj.value("tags")); - - const QJsonObject matchObj = obj.value("match").toObject(); - cfg.match.filePatterns = stringArray(matchObj.value("file_patterns")); - cfg.match.pathPatterns = stringArray(matchObj.value("path_patterns")); - cfg.match.projectNames = stringArray(matchObj.value("project_names")); - - cfg.extendsName = obj.value("extends").toString(); - cfg.abstract = obj.value("abstract").toBool(false); - cfg.hidden = obj.value("hidden").toBool(false); - - const QJsonObject tpl = obj.value("template").toObject(); - cfg.messageFormat = tpl.value("message_format").toString(); - cfg.sampling = tpl.value("sampling").toObject(); - cfg.thinking = tpl.value("thinking").toObject(); - return cfg; -} - -struct RawEntry -{ - QJsonObject obj; - QString filePath; - bool overridesBundled = false; -}; - -constexpr int kMaxExtendsDepth = 32; - -QJsonObject resolveExtends( - const QString &name, - const QHash &raw, - QSet &visiting, - QStringList &errors, - int depth = 0) -{ - if (depth > kMaxExtendsDepth) { - errors.append(QStringLiteral("Agent extends chain too deep (>%1) at '%2'") - .arg(kMaxExtendsDepth) - .arg(name)); - return {}; - } - if (visiting.contains(name)) { - errors.append(QStringLiteral("Cyclic 'extends' involving agent '%1'").arg(name)); - return {}; - } - if (!raw.contains(name)) { - errors.append(QStringLiteral("Unknown parent agent '%1'").arg(name)); - return {}; - } - visiting.insert(name); - - QJsonObject self = raw.value(name).obj; - const QString parent = self.value("extends").toString(); - if (!parent.isEmpty()) { - const QJsonObject parentMerged - = resolveExtends(parent, raw, visiting, errors, depth + 1); - QJsonObject merged = deepMerge(parentMerged, self); - merged["name"] = name; - if (self.contains("abstract")) - merged["abstract"] = self.value("abstract"); - else - merged.remove("abstract"); - self = merged; - } - visiting.remove(name); - return self; -} - -} // namespace - -std::optional AgentLoader::parseFile( - const QString &path, QString *error, QStringList * /*warnings*/) -{ - auto objOpt = parseTomlFile(path, error); - if (!objOpt) return std::nullopt; - AgentConfig cfg = configFromMerged(*objOpt); - cfg.sourcePath = path; - return cfg; -} - -AgentLoader::LoadResult AgentLoader::load(const QString &qrcPrefix, const QString &userDir) -{ - LoadResult result; - QHash raw; - - auto scan = [&](const QString &dir, bool isUserLayer) { - if (dir.isEmpty()) return; - QDir d(dir); - if (!d.exists()) return; - const QStringList files = d.entryList({"*.toml"}, QDir::Files); - for (const QString &fname : files) { - const QString fullPath = d.filePath(fname); - QString err; - auto objOpt = parseTomlFile(fullPath, &err); - if (!objOpt) { - result.errors.append(err); - continue; - } - const QString name = objOpt->value("name").toString(); - if (name.isEmpty()) { - result.errors.append(QStringLiteral("Agent at %1 has no 'name'").arg(fullPath)); - continue; - } - const bool overrides = isUserLayer && raw.contains(name); - raw.insert(name, {*objOpt, fullPath, overrides}); - } - }; - - scan(qrcPrefix, /*isUserLayer=*/false); - scan(userDir, /*isUserLayer=*/true); - - for (auto it = raw.constBegin(); it != raw.constEnd(); ++it) { - const QString &name = it.key(); - - QSet visiting; - const QJsonObject merged = resolveExtends(name, raw, visiting, result.errors); - if (merged.isEmpty()) continue; - - AgentConfig cfg = configFromMerged(merged); - cfg.sourcePath = it.value().filePath; - cfg.overridesBundled = it.value().overridesBundled; - - if (cfg.abstract) continue; - - const QString validation = AgentConfig::validate(cfg); - if (!validation.isEmpty()) { - result.errors.append(validation); - continue; - } - result.configs.push_back(std::move(cfg)); - } - - std::sort(result.configs.begin(), result.configs.end(), - [](const AgentConfig &a, const AgentConfig &b) { return a.name < b.name; }); - return result; -} - -} // namespace QodeAssist::Agents diff --git a/sources/agents/AgentLoader.hpp b/sources/agents/AgentLoader.hpp deleted file mode 100644 index 9f26eca..0000000 --- a/sources/agents/AgentLoader.hpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include - -#include "AgentConfig.hpp" - -namespace QodeAssist::Agents { - -class AgentLoader -{ -public: - struct LoadResult - { - std::vector configs; - QStringList errors; - QStringList warnings; - }; - - static LoadResult load(const QString &qrcPrefix, const QString &userDir); - - static std::optional parseFile( - const QString &path, QString *error, QStringList *warnings = nullptr); -}; - -} // namespace QodeAssist::Agents diff --git a/sources/agents/AgentRouter.cpp b/sources/agents/AgentRouter.cpp deleted file mode 100644 index 7135eb4..0000000 --- a/sources/agents/AgentRouter.cpp +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "AgentRouter.hpp" - -#include -#include - -#include "AgentFactory.hpp" - -namespace QodeAssist::AgentRouter { - -namespace { - -bool matchesAnyGlob(const QStringList &patterns, const QString &subject) -{ - if (subject.isEmpty()) - return false; - for (const QString &pat : patterns) { - const QRegularExpression re( - QRegularExpression::anchoredPattern( - QRegularExpression::wildcardToRegularExpression( - pat, QRegularExpression::NonPathWildcardConversion)), - QRegularExpression::CaseInsensitiveOption); - if (re.isValid() && re.match(subject).hasMatch()) - return true; - } - return false; -} - -bool matchesFilePatterns(const QStringList &patterns, const QString &filePath) -{ - if (patterns.isEmpty()) - return true; - if (filePath.isEmpty()) - return false; - const QString name = QFileInfo(filePath).fileName(); - return matchesAnyGlob(patterns, name) || matchesAnyGlob(patterns, filePath); -} - -bool matchesPathPatterns(const QStringList &patterns, const QString &filePath) -{ - if (patterns.isEmpty()) - return true; - if (filePath.isEmpty()) - return false; - return matchesAnyGlob(patterns, filePath); -} - -bool matchesProjectNames(const QStringList &names, const QString &projectName) -{ - if (names.isEmpty()) - return true; // dimension unconstrained - if (projectName.isEmpty()) - return false; - // Project names are user-facing identifiers, not paths — case - // sensitive comparison matches what ProjectExplorer hands us. - return names.contains(projectName); -} - -} // namespace - -bool matches(const AgentConfig::Match &m, const Context &ctx) -{ - if (m.isEmpty()) - return true; // explicit catch-all - return matchesFilePatterns(m.filePatterns, ctx.filePath) - && matchesPathPatterns(m.pathPatterns, ctx.filePath) - && matchesProjectNames(m.projectNames, ctx.projectName); -} - -QString pickAgent( - const QStringList &roster, const Context &ctx, const AgentFactory &factory) -{ - for (const QString &name : roster) { - const AgentConfig *cfg = factory.configByName(name); - if (!cfg) - continue; // stale roster entry — silently skip - if (matches(cfg->match, ctx)) - return name; - } - return {}; -} - -} // namespace QodeAssist::AgentRouter diff --git a/sources/agents/AgentRouter.hpp b/sources/agents/AgentRouter.hpp deleted file mode 100644 index 78df915..0000000 --- a/sources/agents/AgentRouter.hpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -#include "AgentConfig.hpp" - -namespace QodeAssist { - -class AgentFactory; - -namespace AgentRouter { - -struct Context -{ - QString filePath; - QString projectName; -}; - -[[nodiscard]] bool matches(const AgentConfig::Match &match, const Context &ctx); - -[[nodiscard]] QString pickAgent( - const QStringList &roster, const Context &ctx, const AgentFactory &factory); - -} // namespace AgentRouter - -} // namespace QodeAssist diff --git a/sources/agents/CMakeLists.txt b/sources/agents/CMakeLists.txt deleted file mode 100644 index c9a8586..0000000 --- a/sources/agents/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -add_library(Agents STATIC - AgentConfig.hpp - Agent.hpp Agent.cpp - AgentLoader.hpp AgentLoader.cpp - AgentFactory.hpp AgentFactory.cpp - AgentRouter.hpp AgentRouter.cpp - ContextRenderer.hpp ContextRenderer.cpp - agents.qrc -) - -target_link_libraries(Agents - PUBLIC - Qt::Core - Qt::Network - QtCreator::Core - QtCreator::Utils - LLMQore - pantor::inja - ProvidersConfig - Common - Providers - Templates - PRIVATE - QodeAssistLogger - tomlplusplus::tomlplusplus -) - -target_include_directories(Agents - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} -) diff --git a/sources/agents/ContextRenderer.cpp b/sources/agents/ContextRenderer.cpp deleted file mode 100644 index abdcc66..0000000 --- a/sources/agents/ContextRenderer.cpp +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "ContextRenderer.hpp" - -#include -#include -#include -#include -#include - -#include - -#include - -namespace QodeAssist::Templates::ContextRenderer { - -namespace { - -QString substituteVars(const QString &src, const Bindings &b) -{ - QString out = src; - if (!b.projectDir.isEmpty()) - out.replace(QStringLiteral("${PROJECT_DIR}"), b.projectDir); - if (!b.homeDir.isEmpty()) - out.replace(QStringLiteral("${HOME}"), b.homeDir); - return out; -} - -bool isPathAllowed(const QString &requestedPath, const Bindings &b) -{ - const QString target = QDir::cleanPath(requestedPath); - - auto isUnder = [&target](const QString &root) { - if (root.isEmpty()) return false; - const QString cleanRoot = QDir::cleanPath(root); - if (target == cleanRoot) return true; - return target.startsWith(cleanRoot + QLatin1Char('/')); - }; - - if (isUnder(b.projectDir)) return true; - if (!b.homeDir.isEmpty() && isUnder(b.homeDir + QStringLiteral("/qodeassist"))) - return true; - return false; -} - -void registerReadFile(inja::Environment &env, const Bindings &b) -{ - const Bindings capturedBindings = b; - env.add_callback("read_file", 1, [capturedBindings](inja::Arguments &args) -> nlohmann::json { - const std::string raw = args.at(0)->get(); - QString path = QString::fromStdString(raw); - - if (!capturedBindings.projectDir.isEmpty()) - path.replace(QStringLiteral("${PROJECT_DIR}"), capturedBindings.projectDir); - if (!capturedBindings.homeDir.isEmpty()) - path.replace(QStringLiteral("${HOME}"), capturedBindings.homeDir); - - if (!isPathAllowed(path, capturedBindings)) { - qWarning("[QodeAssist] context.read_file: path not in allowed roots: %s", - qUtf8Printable(path)); - return std::string{}; - } - QFile f(path); - if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) - return std::string{}; - return f.readAll().toStdString(); - }); -} - -QString expandAndResolvePath(const QString &raw, const Bindings &b) -{ - QString p = raw; - if (!b.projectDir.isEmpty()) - p.replace(QStringLiteral("${PROJECT_DIR}"), b.projectDir); - if (!b.homeDir.isEmpty()) - p.replace(QStringLiteral("${HOME}"), b.homeDir); - return p; -} - -void registerFileExists(inja::Environment &env, const Bindings &b) -{ - const Bindings caps = b; - env.add_callback("file_exists", 1, [caps](inja::Arguments &args) -> nlohmann::json { - const QString p = expandAndResolvePath( - QString::fromStdString(args.at(0)->get()), caps); - if (!isPathAllowed(p, caps)) - return false; - return QFileInfo::exists(p); - }); -} - -void registerReadDir(inja::Environment &env, const Bindings &b) -{ - const Bindings caps = b; - - env.add_callback("read_dir", 1, [caps](inja::Arguments &args) -> nlohmann::json { - const QString p = expandAndResolvePath( - QString::fromStdString(args.at(0)->get()), caps); - if (!isPathAllowed(p, caps)) { - qWarning("[QodeAssist] context.read_dir: path not in allowed roots: %s", - qUtf8Printable(p)); - return nlohmann::json::array(); - } - QDir dir(p); - if (!dir.exists()) - return nlohmann::json::array(); - nlohmann::json out = nlohmann::json::array(); - const QStringList entries = dir.entryList( - QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); - for (const QString &name : entries) - out.push_back(name.toStdString()); - return out; - }); -} - -void registerStringHelpers(inja::Environment &env) -{ - env.add_callback("head_lines", 2, [](inja::Arguments &args) -> nlohmann::json { - const QString text = QString::fromStdString(args.at(0)->get()); - const int n = args.at(1)->get(); - if (n <= 0) - return std::string{}; - const QStringList lines = text.split('\n'); - const int take = std::min(n, lines.size()); - QStringList head; - head.reserve(take); - for (int i = 0; i < take; ++i) - head.append(lines.at(i)); - return head.join('\n').toStdString(); - }); - - env.add_callback("basename", 1, [](inja::Arguments &args) -> nlohmann::json { - return QFileInfo(QString::fromStdString(args.at(0)->get())) - .fileName() - .toStdString(); - }); - env.add_callback("dirname", 1, [](inja::Arguments &args) -> nlohmann::json { - return QFileInfo(QString::fromStdString(args.at(0)->get())) - .path() - .toStdString(); - }); - env.add_callback("ext", 1, [](inja::Arguments &args) -> nlohmann::json { - return QFileInfo(QString::fromStdString(args.at(0)->get())) - .suffix() - .toStdString(); - }); - - env.add_callback("lower", 1, [](inja::Arguments &args) -> nlohmann::json { - return QString::fromStdString(args.at(0)->get()).toLower().toStdString(); - }); - env.add_callback("upper", 1, [](inja::Arguments &args) -> nlohmann::json { - return QString::fromStdString(args.at(0)->get()).toUpper().toStdString(); - }); -} - -void registerSandbox(inja::Environment &env) -{ - - env.set_search_included_templates_in_files(false); - env.set_include_callback( - [](const std::filesystem::path &, const std::string &name) -> inja::Template { - throw inja::FileError( - "include is disabled in QodeAssist context: '" + name + "'"); - }); - - env.set_line_statement("@@@inja@@@"); -} - -} // namespace - -QString render(const QString &templateSource, const Bindings &bindings, QString *error) -{ - if (templateSource.isEmpty()) - return {}; - - const QString substituted = substituteVars(templateSource, bindings); - - inja::Environment env; - registerSandbox(env); - registerReadFile(env, bindings); - registerFileExists(env, bindings); - registerReadDir(env, bindings); - registerStringHelpers(env); - - inja::Template tpl; - try { - tpl = env.parse(substituted.toStdString()); - } catch (const std::exception &e) { - if (error) { - *error = QStringLiteral("Failed to parse context jinja: %1") - .arg(QString::fromUtf8(e.what())); - } - return {}; - } - - try { - const std::string rendered = env.render(tpl, nlohmann::json::object()); - return QString::fromStdString(rendered); - } catch (const std::exception &e) { - if (error) { - *error = QStringLiteral("Failed to render context jinja: %1") - .arg(QString::fromUtf8(e.what())); - } - return {}; - } -} - -} // namespace QodeAssist::Templates::ContextRenderer diff --git a/sources/agents/ContextRenderer.hpp b/sources/agents/ContextRenderer.hpp deleted file mode 100644 index e4fb2fd..0000000 --- a/sources/agents/ContextRenderer.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -namespace QodeAssist::Templates::ContextRenderer { - -struct Bindings -{ - QString projectDir; - QString homeDir; -}; - -QString render(const QString &templateSource, const Bindings &bindings, - QString *error = nullptr); - -} // namespace QodeAssist::Templates::ContextRenderer diff --git a/sources/agents/agents.qrc b/sources/agents/agents.qrc deleted file mode 100644 index 8bf4c2d..0000000 --- a/sources/agents/agents.qrc +++ /dev/null @@ -1,9 +0,0 @@ - - - ollama_base_chat.toml - ollama_base_fim.toml - ollama_gemma4_e4b_chat.toml - ollama_codellama_7b_code_fim.toml - ollama_codellama_13b_qml_fim.toml - - diff --git a/sources/agents/ollama_base_chat.toml b/sources/agents/ollama_base_chat.toml deleted file mode 100644 index 5c89228..0000000 --- a/sources/agents/ollama_base_chat.toml +++ /dev/null @@ -1,44 +0,0 @@ -schema_version = 1 - -name = "Ollama Base Chat" -description = "Shared base for Ollama /api/chat profiles." - -abstract = true - -provider_instance = "Ollama (Native)" -endpoint = "/api/chat" - -tags = ["ollama", "local"] - -[template] -message_format = """ -{ - "messages": [ - {%- if existsIn(ctx, "system_prompt") %} - { - "role": "system", - "content": {{ tojson(ctx.system_prompt) }} - }{% if length(ctx.history) > 0 %},{% endif %} - {%- endif %} - {%- for msg in ctx.history %} - { - "role": {{ tojson(msg.role) }}, - "content": {{ tojson(msg.content) }}{% if existsIn(msg, "images") %}, - "images": [ - {%- for img in msg.images %} - {{ tojson(img.data) }}{% if not loop.is_last %},{% endif %} - {%- endfor %} - ]{% endif %} - }{% if not loop.is_last %},{% endif %} - {%- endfor %} - ] -} -""" - -[template.sampling] -stream = true - -[template.sampling.options] -num_predict = 2048 -temperature = 0.7 -keep_alive = "5m" diff --git a/sources/agents/ollama_base_fim.toml b/sources/agents/ollama_base_fim.toml deleted file mode 100644 index 72b9d2f..0000000 --- a/sources/agents/ollama_base_fim.toml +++ /dev/null @@ -1,32 +0,0 @@ -schema_version = 1 - -name = "Ollama FIM Base" -description = "Shared base for Ollama native FIM (/api/generate) profiles." - -abstract = true - -provider_instance = "Ollama (Native)" -endpoint = "/api/generate" - -tags = ["ollama", "local", "fim"] - -[template] -message_format = """ -{ - "prompt": {{ tojson(ctx.prefix) }}, - "suffix": {{ tojson(ctx.suffix) }} - {%- if existsIn(ctx, "system_prompt") %}, - "system": {{ tojson(ctx.system_prompt) }} - {%- endif %} -} -""" - -[template.sampling] -stream = true - -[template.sampling.options] -num_predict = 512 -temperature = 0.2 -top_p = 0.9 -keep_alive = "5m" -stop = [""] diff --git a/sources/agents/ollama_codellama_13b_qml_fim.toml b/sources/agents/ollama_codellama_13b_qml_fim.toml deleted file mode 100644 index b7538a9..0000000 --- a/sources/agents/ollama_codellama_13b_qml_fim.toml +++ /dev/null @@ -1,40 +0,0 @@ -schema_version = 1 - -name = "Qt CodeLlama 13B QML FIM" -description = "Local Qt-Company-tuned CodeLlama 13B for QML FIM completion." - -provider_instance = "Ollama (Native)" -endpoint = "/api/generate" - -model = "theqtcompany/codellama-13b-qml:latest" - -tags = ["fim", "ollama", "local", "codellama", "qml", "qt"] - -[match] -file_patterns = ["*.qml"] - -[template] -message_format = """ -{ - "prompt": {%- if existsIn(ctx, "suffix") and length(ctx.suffix) > 0 -%} - {{ tojson("" + ctx.suffix + "
" + ctx.prefix + "") }}
-  {%- else -%}
-    {{ tojson("
" + ctx.prefix + "") }}
-  {%- endif %}
-  {%- if existsIn(ctx, "system_prompt") %},
-  "system": {{ tojson(ctx.system_prompt) }}
-  {%- endif %}
-}
-"""
-
-[template.sampling]
-stream = true
-
-[template.sampling.options]
-num_predict    = 500
-temperature    = 0
-top_p          = 1
-repeat_penalty = 1.05
-keep_alive  = "5m"
-
-stop = ["", "
", "
", "
", "< EOT >", "\\end", "", "", "##"] diff --git a/sources/agents/ollama_codellama_7b_code_fim.toml b/sources/agents/ollama_codellama_7b_code_fim.toml deleted file mode 100644 index 235ef4a..0000000 --- a/sources/agents/ollama_codellama_7b_code_fim.toml +++ /dev/null @@ -1,34 +0,0 @@ -schema_version = 1 - -name = "CodeLlama 7B Code FIM" -description = "Local CodeLlama 7B (code variant) on Ollama, FIM completion via PRE/SUF/MID markers." - -provider_instance = "Ollama (Native)" -endpoint = "/api/generate" - -model = "codellama:7b-code" - -tags = ["fim", "ollama", "local", "codellama"] - -[match] -file_patterns = ["*.cpp", "*.cc", "*.cxx", "*.c", "*.h", "*.hpp", "*.hxx", "*.inl"] - -[template] -message_format = """ -{ - "prompt": {{ tojson("
 " + ctx.prefix + " " + ctx.suffix + " ") }}
-  {%- if existsIn(ctx, "system_prompt") %},
-  "system": {{ tojson(ctx.system_prompt) }}
-  {%- endif %}
-}
-"""
-
-[template.sampling]
-stream = true
-
-[template.sampling.options]
-num_predict = 512
-temperature = 0.2
-top_p       = 0.9
-keep_alive  = "5m"
-stop = ["", "
", "", ""]
diff --git a/sources/agents/ollama_gemma4_e4b_chat.toml b/sources/agents/ollama_gemma4_e4b_chat.toml
deleted file mode 100644
index e41fe22..0000000
--- a/sources/agents/ollama_gemma4_e4b_chat.toml
+++ /dev/null
@@ -1,36 +0,0 @@
-schema_version = 1
-
-name    = "Ollama gemma4:e4b Chat"
-extends = "Ollama Base Chat"
-
-description = "Local Gemma 4 E4B on Ollama /api/chat — coding chat assistant."
-
-model = "gemma4:e4b"
-
-role = """
-You are a helpful coding assistant integrated into Qt Creator.
-Answer concisely. When the user shares code, prefer concrete diffs or
-minimal patches over rewriting whole files. Use markdown code blocks
-with language tags so the IDE can render them.
-"""
-
-enable_thinking = true
-enable_tools    = true
-
-tags = ["chat", "ollama", "local", "gemma"]
-
-context = """
-{%- set readme      = read_file("${PROJECT_DIR}/README.md")        -%}
-
-{%- if length(readme) > 0 %}
-## Project README.md
-{{ readme }}
-{%- endif %}
-"""
-
-[template.sampling.options]
-num_predict = 4096
-temperature = 1
-top_k       = 64
-top_p       = 0.95
-num_ctx     = 8192
diff --git a/sources/common/CMakeLists.txt b/sources/common/CMakeLists.txt
deleted file mode 100644
index fdb1a7b..0000000
--- a/sources/common/CMakeLists.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-add_library(Common INTERFACE)
-
-target_sources(Common INTERFACE
-    ContextData.hpp
-)
-
-target_include_directories(Common INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
-
-target_link_libraries(Common INTERFACE
-    Qt::Core
-)
diff --git a/sources/common/ContextData.hpp b/sources/common/ContextData.hpp
deleted file mode 100644
index 650353b..0000000
--- a/sources/common/ContextData.hpp
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-
-namespace QodeAssist::Templates {
-
-struct ContentBlockEntry
-{
-    enum class Kind {
-        Text,
-        Thinking,
-        RedactedThinking,
-        ToolUse,
-        ToolResult,
-        Image,
-    };
-
-    Kind kind = Kind::Text;
-
-    QString text;          // Text
-    QString thinking;      // Thinking
-    QString signature;     // Thinking / RedactedThinking
-    QString toolUseId;     // ToolUse / ToolResult
-    QString toolName;      // ToolUse
-    QJsonObject toolInput; // ToolUse
-    QString result;        // ToolResult
-    QString imageData;     // Image (base64 or url)
-    QString mediaType;     // Image
-    bool isImageUrl = false;
-
-    bool operator==(const ContentBlockEntry &) const = default;
-};
-
-struct Message
-{
-    QString role;
-    QVector blocks;
-
-    // Convenience for callers that only need a single text block.
-    static Message text(const QString &role, const QString &text)
-    {
-        Message m;
-        m.role = role;
-        ContentBlockEntry e;
-        e.kind = ContentBlockEntry::Kind::Text;
-        e.text = text;
-        m.blocks.append(std::move(e));
-        return m;
-    }
-
-    bool operator==(const Message &) const = default;
-};
-
-struct FileMetadata
-{
-    QString filePath;
-    QString content;
-    bool operator==(const FileMetadata &) const = default;
-};
-
-struct ContextData
-{
-    std::optional systemPrompt = std::nullopt;
-    std::optional prefix = std::nullopt;
-    std::optional suffix = std::nullopt;
-    std::optional fileContext = std::nullopt;
-    std::optional> history = std::nullopt;
-    std::optional> filesMetadata = std::nullopt;
-
-    bool operator==(const ContextData &) const = default;
-};
-
-} // namespace QodeAssist::Templates
diff --git a/sources/providers/CMakeLists.txt b/sources/providers/CMakeLists.txt
deleted file mode 100644
index ce2e329..0000000
--- a/sources/providers/CMakeLists.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-add_library(Providers STATIC
-    ProviderID.hpp
-    Provider.hpp Provider.cpp
-    ProviderFactory.hpp ProviderFactory.cpp
-)
-
-target_link_libraries(Providers
-    PUBLIC
-        Qt::Core
-        Qt::Network
-        QtCreator::Core
-        QtCreator::Utils
-        LLMQore
-        Common
-    PRIVATE
-        QodeAssistLogger
-)
-
-target_include_directories(Providers
-    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
-    PRIVATE ${CMAKE_SOURCE_DIR}/sources/templates
-)
diff --git a/sources/providers/Provider.cpp b/sources/providers/Provider.cpp
deleted file mode 100644
index a83c785..0000000
--- a/sources/providers/Provider.cpp
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "Provider.hpp"
-
-#include "PromptTemplate.hpp"
-
-#include 
-#include 
-
-#include 
-#include 
-
-#include 
-
-namespace QodeAssist::Providers {
-
-Provider::Provider(QObject *parent)
-    : QObject(parent)
-{}
-
-bool Provider::prepareRequest(
-    QJsonObject &request,
-    PromptTemplate *prompt,
-    const ContextData &context,
-    bool isToolsEnabled,
-    bool isThinkingEnabled)
-{
-    if (!prompt) {
-        LOG_MESSAGE(QString("Provider '%1': null template").arg(name()));
-        return false;
-    }
-
-    if (!prompt->isSupportProvider(providerID())) {
-        LOG_MESSAGE(QString("Template '%1' doesn't support provider '%2'")
-                        .arg(prompt->name(), name()));
-        return false;
-    }
-
-    if (!prompt->buildFullRequest(request, context, isThinkingEnabled)) {
-        LOG_MESSAGE(
-            QString("Provider '%1': template '%2' failed to build request")
-                .arg(name(), prompt->name()));
-        return false;
-    }
-
-    if (isToolsEnabled) {
-        const auto toolsDefinitions = toolsManager()->getToolsDefinitions();
-        if (!toolsDefinitions.isEmpty()) {
-            request["tools"] = toolsDefinitions;
-        }
-    }
-    return true;
-}
-
-RequestID Provider::sendRequest(
-    const QUrl &url, const QJsonObject &payload, const QString &endpoint)
-{
-    auto *c = client();
-
-    c->setUrl(url.toString());
-    c->setApiKey(apiKey());
-
-    auto requestId = c->sendMessage(payload, endpoint);
-
-    LOG_MESSAGE(
-        QString("%1: Sending request %2 to %3%4").arg(name(), requestId, url.toString(), endpoint));
-    LOG_MESSAGE(
-        QString("%1: Payload:\n%2")
-            .arg(name(), QString::fromUtf8(QJsonDocument(payload).toJson(QJsonDocument::Indented))));
-
-    return requestId;
-}
-
-void Provider::cancelRequest(const RequestID &requestId)
-{
-    LOG_MESSAGE(QString("%1: Cancelling request %2").arg(name(), requestId));
-    client()->cancelRequest(requestId);
-}
-
-::LLMQore::ToolsManager *Provider::toolsManager() const
-{
-    return client()->tools();
-}
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providers/Provider.hpp b/sources/providers/Provider.hpp
deleted file mode 100644
index 633035a..0000000
--- a/sources/providers/Provider.hpp
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "ContextData.hpp"
-#include "ProviderID.hpp"
-#include "LLMQore/BaseClient.hpp"
-
-namespace LLMQore {
-class BaseClient;
-class ToolsManager;
-}
-
-namespace QodeAssist::Templates {
-class PromptTemplate;
-}
-
-class QJsonObject;
-
-namespace QodeAssist::Providers {
-
-using Templates::ContextData;
-using Templates::PromptTemplate;
-using LLMQore::RequestID;
-
-enum class ProviderCapability {
-    Tools        = 0x1,
-    Thinking     = 0x2,
-    Image        = 0x4,
-    ModelListing = 0x8,
-};
-Q_DECLARE_FLAGS(ProviderCapabilities, ProviderCapability)
-Q_DECLARE_OPERATORS_FOR_FLAGS(ProviderCapabilities)
-
-class Provider : public QObject
-{
-    Q_OBJECT
-    Q_DISABLE_COPY_MOVE(Provider)
-public:
-    explicit Provider(QObject *parent = nullptr);
-
-    virtual ~Provider() = default;
-
-    virtual QString name() const = 0;
-
-    virtual QString url() const { return m_url; }
-    virtual QString apiKey() const { return m_apiKey; }
-    void setUrl(const QString &url) { m_url = url; }
-    void setApiKey(const QString &apiKey) { m_apiKey = apiKey; }
-
-    [[nodiscard]] virtual bool prepareRequest(
-        QJsonObject &request,
-        PromptTemplate *prompt,
-        const ContextData &context,
-        bool isToolsEnabled,
-        bool isThinkingEnabled);
-    virtual QFuture> getInstalledModels(const QString &url) = 0;
-    virtual ProviderID providerID() const = 0;
-    virtual ProviderCapabilities capabilities() const { return {}; }
-
-    virtual ::LLMQore::BaseClient *client() const = 0;
-
-    virtual RequestID sendRequest(
-        const QUrl &url, const QJsonObject &payload, const QString &endpoint);
-    void cancelRequest(const RequestID &requestId);
-    ::LLMQore::ToolsManager *toolsManager() const;
-
-private:
-    QString m_url;
-    QString m_apiKey;
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providers/ProviderFactory.cpp b/sources/providers/ProviderFactory.cpp
deleted file mode 100644
index 69a7a34..0000000
--- a/sources/providers/ProviderFactory.cpp
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "ProviderFactory.hpp"
-
-#include 
-
-namespace QodeAssist::Providers::ProviderFactory {
-
-namespace {
-
-QHash &table()
-{
-    static QHash t;
-    return t;
-}
-
-} // namespace
-
-void registerType(const QString &name, FactoryFn fn)
-{
-    if (name.isEmpty() || !fn) return;
-    table().insert(name, std::move(fn));
-}
-
-Provider *create(const QString &name, QObject *parent)
-{
-    auto it = table().constFind(name);
-    if (it == table().constEnd()) return nullptr;
-    return it.value()(parent);
-}
-
-QStringList knownNames()
-{
-    return table().keys();
-}
-
-void clear()
-{
-    table().clear();
-}
-
-} // namespace QodeAssist::Providers::ProviderFactory
diff --git a/sources/providers/ProviderFactory.hpp b/sources/providers/ProviderFactory.hpp
deleted file mode 100644
index 899bf2e..0000000
--- a/sources/providers/ProviderFactory.hpp
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-
-namespace QodeAssist::Providers {
-
-class Provider;
-
-namespace ProviderFactory {
-
-using FactoryFn = std::function;
-
-void registerType(const QString &name, FactoryFn fn);
-Provider *create(const QString &name, QObject *parent);
-QStringList knownNames();
-void clear(); // for tests / shutdown
-
-} // namespace ProviderFactory
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providers/ProviderID.hpp b/sources/providers/ProviderID.hpp
deleted file mode 100644
index 7365f38..0000000
--- a/sources/providers/ProviderID.hpp
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-namespace QodeAssist::Providers {
-
-enum class ProviderID : int {
-    Any,
-    Ollama,
-    LMStudio,
-    Claude,
-    OpenAI,
-    OpenAICompatible,
-    OpenAIResponses,
-    MistralAI,
-    OpenRouter,
-    GoogleAI,
-    LlamaCpp,
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/CMakeLists.txt b/sources/providersConfig/CMakeLists.txt
deleted file mode 100644
index ef14835..0000000
--- a/sources/providersConfig/CMakeLists.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-add_library(ProvidersConfig STATIC
-    ProviderInstance.hpp ProviderInstance.cpp
-    ProviderInstanceLoader.hpp ProviderInstanceLoader.cpp
-    ProviderInstanceWriter.hpp ProviderInstanceWriter.cpp
-    ProviderInstanceFactory.hpp ProviderInstanceFactory.cpp
-    ProviderSecretsStore.hpp ProviderSecretsStore.cpp
-    ProviderLauncher.hpp ProviderLauncher.cpp
-
-    provider_instances.qrc
-)
-
-target_link_libraries(ProvidersConfig
-    PUBLIC
-        Qt::Core
-        Qt::Network
-        QtCreator::Core
-        QtCreator::Utils
-        QtCreator::TerminalLib
-    PRIVATE
-        QodeAssistLogger
-        TomlSerializer
-        tomlplusplus::tomlplusplus
-)
-
-target_include_directories(ProvidersConfig
-    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
-)
diff --git a/sources/providersConfig/ProviderInstance.cpp b/sources/providersConfig/ProviderInstance.cpp
deleted file mode 100644
index 9c614d9..0000000
--- a/sources/providersConfig/ProviderInstance.cpp
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "ProviderInstance.hpp"
-
-#include 
-
-namespace QodeAssist::Providers {
-
-QString ProviderInstance::validate(
-    const ProviderInstance &inst, const QStringList &knownClientApis)
-{
-    if (inst.name.isEmpty())
-        return QStringLiteral("Provider instance has no name");
-    if (inst.clientApi.isEmpty())
-        return QStringLiteral("Provider instance '%1' has no client_api").arg(inst.name);
-    if (!knownClientApis.isEmpty() && !knownClientApis.contains(inst.clientApi)) {
-        return QStringLiteral("Provider instance '%1' references unknown client_api '%2'")
-            .arg(inst.name, inst.clientApi);
-    }
-    if (inst.url.isEmpty())
-        return QStringLiteral("Provider instance '%1' has no URL").arg(inst.name);
-    const QUrl parsed(inst.url);
-    if (!parsed.isValid()
-        || (parsed.scheme() != QLatin1StringView{"http"}
-            && parsed.scheme() != QLatin1StringView{"https"})) {
-        return QStringLiteral("Provider instance '%1' has an invalid or unsafe URL: %2")
-            .arg(inst.name, inst.url);
-    }
-    return {};
-}
-
-QString ProviderInstance::warnings(const ProviderInstance &inst)
-{
-    const QUrl parsed(inst.url);
-    if (parsed.scheme() == QLatin1StringView{"http"} && !inst.apiKeyRef.isEmpty()) {
-        const QString host = parsed.host();
-        const bool isLoopback = host == QLatin1StringView{"localhost"}
-                                || host == QLatin1StringView{"127.0.0.1"}
-                                || host == QLatin1StringView{"::1"};
-        if (!isLoopback) {
-            return QStringLiteral(
-                       "URL uses plaintext http:// to '%1' but the provider has an API key. "
-                       "Any request will transmit the key unencrypted — prefer https://.")
-                .arg(host);
-        }
-    }
-    return {};
-}
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderInstance.hpp b/sources/providersConfig/ProviderInstance.hpp
deleted file mode 100644
index 591a783..0000000
--- a/sources/providersConfig/ProviderInstance.hpp
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-
-namespace QodeAssist::Providers {
-
-struct LaunchConfig
-{
-    QString command;
-    QStringList args;
-    QString cwd;
-    QHash env;
-
-    QString readyUrl;
-    std::chrono::seconds readyTimeout{30};
-
-    bool autoStart = false;
-
-    bool detach = false;
-
-    [[nodiscard]] bool isEmpty() const noexcept { return command.isEmpty(); }
-};
-
-struct ProviderInstance
-{
-    QString name;
-    QString clientApi;
-    QString description;
-    QString url;
-    QString apiKeyRef;
-    QJsonObject extras;
-    LaunchConfig launch;
-    QString extendsName;
-    bool abstract = false;
-
-    QString sourcePath;
-    bool overridesBundled = false;
-    [[nodiscard]] bool isUserSource() const
-    {
-        return !sourcePath.startsWith(QLatin1StringView{":/"});
-    }
-
-    [[nodiscard]] static QString validate(
-        const ProviderInstance &inst, const QStringList &knownClientApis);
-
-    [[nodiscard]] static QString warnings(const ProviderInstance &inst);
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderInstanceFactory.cpp b/sources/providersConfig/ProviderInstanceFactory.cpp
deleted file mode 100644
index 23ecadd..0000000
--- a/sources/providersConfig/ProviderInstanceFactory.cpp
+++ /dev/null
@@ -1,199 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "ProviderInstanceFactory.hpp"
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-
-#include "ProviderInstanceLoader.hpp"
-#include "Logger.hpp"
-
-static inline void initProviderInstancesResource()
-{
-    Q_INIT_RESOURCE(provider_instances);
-}
-
-namespace {
-
-Q_LOGGING_CATEGORY(providerInstanceFactoryLog, "qodeassist.providerinstancefactory")
-
-QString instanceQrcPrefix() { return QStringLiteral(":/provider-instances"); }
-
-} // namespace
-
-namespace QodeAssist::Providers {
-
-ProviderInstanceFactory::ProviderInstanceFactory(QObject *parent)
-    : QObject(parent)
-{
-    ::initProviderInstancesResource();
-
-    m_watcher = new QFileSystemWatcher(this);
-    m_reloadDebounce = new QTimer(this);
-    m_reloadDebounce->setSingleShot(true);
-    m_reloadDebounce->setInterval(150);
-    connect(m_reloadDebounce, &QTimer::timeout, this, [this] { reload(); });
-    auto kick = [this](const QString &) { m_reloadDebounce->start(); };
-    connect(m_watcher, &QFileSystemWatcher::fileChanged, this, kick);
-    connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, kick);
-
-    reload();
-}
-
-ProviderInstanceFactory::~ProviderInstanceFactory() = default;
-
-QString ProviderInstanceFactory::userInstancesDir()
-{
-    return Core::ICore::userResourcePath(
-               QStringLiteral("qodeassist/config/providers")).toFSPathString();
-}
-
-void ProviderInstanceFactory::reload()
-{
-    Q_ASSERT_X(QThread::currentThread() == thread(),
-               Q_FUNC_INFO, "ProviderInstanceFactory must be used from its owner thread");
-    clear();
-
-    auto result = ProviderInstanceLoader::load(instanceQrcPrefix(), userInstancesDir());
-    for (const QString &err : result.errors)
-        LOG_MESSAGE(QString("[ProviderInstances] error: %1").arg(err));
-    for (const QString &warn : result.warnings)
-        LOG_MESSAGE(QString("[ProviderInstances] warning: %1").arg(warn));
-    LOG_MESSAGE(QString("[ProviderInstances] Loaded %1 instances (qrc=%2, user=%3)")
-                    .arg(result.instances.size())
-                    .arg(instanceQrcPrefix(), userInstancesDir()));
-
-    for (auto &inst : result.instances) {
-        LOG_MESSAGE(QString("[ProviderInstances] Loaded: %1 (client_api=%2, url=%3)")
-                        .arg(inst.name, inst.clientApi, inst.url));
-        m_instances.push_back(std::move(inst));
-    }
-    m_errors = std::move(result.errors);
-    m_warnings = std::move(result.warnings);
-
-    rebuildIndexes();
-    rewatchUserDir();
-    emit instancesReloaded();
-}
-
-void ProviderInstanceFactory::rebuildIndexes()
-{
-    m_nameIndex.clear();
-    m_instanceNamesCache.clear();
-    m_knownClientApisCache.clear();
-    m_nameIndex.reserve(static_cast(m_instances.size()));
-    m_instanceNamesCache.reserve(static_cast(m_instances.size()));
-
-    std::sort(m_instances.begin(), m_instances.end(),
-              [](const ProviderInstance &a, const ProviderInstance &b) {
-                  return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
-              });
-
-    QSet seenApis;
-    for (qsizetype i = 0; i < static_cast(m_instances.size()); ++i) {
-        const ProviderInstance &inst = m_instances[i];
-        m_nameIndex.insert(inst.name.toCaseFolded(), i);
-        m_instanceNamesCache.append(inst.name);
-        if (!seenApis.contains(inst.clientApi)) {
-            seenApis.insert(inst.clientApi);
-            m_knownClientApisCache.append(inst.clientApi);
-        }
-    }
-    std::sort(m_knownClientApisCache.begin(), m_knownClientApisCache.end(),
-              [](const QString &a, const QString &b) {
-                  return a.compare(b, Qt::CaseInsensitive) < 0;
-              });
-}
-
-void ProviderInstanceFactory::rewatchUserDir()
-{
-    if (!m_watcher)
-        return;
-
-    const QStringList stale = m_watcher->files() + m_watcher->directories();
-    if (!stale.isEmpty())
-        m_watcher->removePaths(stale);
-
-    const QString userDir = userInstancesDir();
-    QDir().mkpath(userDir);
-    m_watcher->addPath(userDir);
-    QDir d(userDir);
-    for (const QFileInfo &fi : d.entryInfoList({"*.toml"}, QDir::Files))
-        m_watcher->addPath(fi.absoluteFilePath());
-}
-
-void ProviderInstanceFactory::registerInstance(ProviderInstance instance)
-{
-    Q_ASSERT_X(QThread::currentThread() == thread(),
-               Q_FUNC_INFO, "ProviderInstanceFactory must be used from its owner thread");
-    const QString validation = ProviderInstance::validate(instance, knownClientApis());
-    if (!validation.isEmpty()) {
-        qCWarning(providerInstanceFactoryLog).noquote()
-            << "Refusing to register provider instance:" << validation;
-        return;
-    }
-    const QString name = instance.name;
-    for (auto &existing : m_instances) {
-        if (existing.name == name) {
-            existing = std::move(instance);
-            emit instanceChanged(name);
-            return;
-        }
-    }
-    m_instances.push_back(std::move(instance));
-    rebuildIndexes();
-    emit instanceChanged(name);
-}
-
-const ProviderInstance *ProviderInstanceFactory::instanceByName(const QString &name) const
-{
-    const auto it = m_nameIndex.constFind(name.toCaseFolded());
-    if (it == m_nameIndex.constEnd())
-        return nullptr;
-    return &m_instances[it.value()];
-}
-
-QStringList ProviderInstanceFactory::instanceNames() const
-{
-    return m_instanceNamesCache;
-}
-
-QStringList ProviderInstanceFactory::instanceNamesForClientApi(const QString &clientApi) const
-{
-    QStringList out;
-    for (const auto &inst : m_instances) {
-        if (inst.clientApi == clientApi)
-            out.append(inst.name);
-    }
-    return out;
-}
-
-QStringList ProviderInstanceFactory::knownClientApis() const
-{
-    return m_knownClientApisCache;
-}
-
-void ProviderInstanceFactory::clear()
-{
-    Q_ASSERT_X(QThread::currentThread() == thread(),
-               Q_FUNC_INFO, "ProviderInstanceFactory must be used from its owner thread");
-    m_instances.clear();
-    m_nameIndex.clear();
-    m_instanceNamesCache.clear();
-    m_knownClientApisCache.clear();
-    m_errors.clear();
-    m_warnings.clear();
-}
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderInstanceFactory.hpp b/sources/providersConfig/ProviderInstanceFactory.hpp
deleted file mode 100644
index 6163a32..0000000
--- a/sources/providersConfig/ProviderInstanceFactory.hpp
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-
-#include "ProviderInstance.hpp"
-
-class QFileSystemWatcher;
-class QTimer;
-
-namespace QodeAssist::Providers {
-
-class ProviderInstanceFactory : public QObject
-{
-    Q_OBJECT
-    Q_DISABLE_COPY_MOVE(ProviderInstanceFactory)
-public:
-    explicit ProviderInstanceFactory(QObject *parent = nullptr);
-    ~ProviderInstanceFactory() override;
-
-    void reload();
-
-    [[nodiscard]] static QString userInstancesDir();
-
-    [[nodiscard]] const ProviderInstance *instanceByName(const QString &name) const;
-    [[nodiscard]] QStringList instanceNames() const;
-    [[nodiscard]] QStringList instanceNamesForClientApi(const QString &clientApi) const;
-    [[nodiscard]] QStringList knownClientApis() const;
-    [[nodiscard]] const std::vector &instances() const noexcept
-    {
-        return m_instances;
-    }
-
-    [[nodiscard]] QStringList lastLoadErrors() const { return m_errors; }
-    [[nodiscard]] QStringList lastLoadWarnings() const { return m_warnings; }
-
-    void registerInstance(ProviderInstance instance);
-    void clear();
-
-signals:
-    void instanceChanged(const QString &name);
-    void instancesReloaded();
-
-private:
-    void rewatchUserDir();
-    void rebuildIndexes();
-
-    std::vector m_instances;
-    QHash m_nameIndex;
-    QStringList m_instanceNamesCache;
-    QStringList m_knownClientApisCache;
-    QStringList m_errors;
-    QStringList m_warnings;
-
-
-    QFileSystemWatcher *m_watcher = nullptr;
-    QTimer *m_reloadDebounce = nullptr;
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderInstanceLoader.cpp b/sources/providersConfig/ProviderInstanceLoader.cpp
deleted file mode 100644
index 223f68f..0000000
--- a/sources/providersConfig/ProviderInstanceLoader.cpp
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "ProviderInstanceLoader.hpp"
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-#include 
-
-namespace QodeAssist::Providers {
-
-namespace {
-
-QJsonValue tomlToJson(const toml::node &node)
-{
-    if (auto *table = node.as_table()) {
-        QJsonObject obj;
-        for (const auto &[key, value] : *table) {
-            obj.insert(QString::fromStdString(std::string{key.str()}), tomlToJson(value));
-        }
-        return obj;
-    }
-    if (auto *array = node.as_array()) {
-        QJsonArray arr;
-        for (const auto &item : *array)
-            arr.append(tomlToJson(item));
-        return arr;
-    }
-    if (auto *str = node.as_string())
-        return QString::fromStdString(str->get());
-    if (auto *integer = node.as_integer())
-        return static_cast(integer->get());
-    if (auto *floating = node.as_floating_point())
-        return floating->get();
-    if (auto *boolean = node.as_boolean())
-        return boolean->get();
-    return QJsonValue::Null;
-}
-
-QJsonObject deepMerge(const QJsonObject &base, const QJsonObject &overlay)
-{
-    QJsonObject result = base;
-    for (auto it = overlay.constBegin(); it != overlay.constEnd(); ++it) {
-        const QJsonValue baseVal = result.value(it.key());
-        const QJsonValue overlayVal = it.value();
-        if (baseVal.isObject() && overlayVal.isObject())
-            result[it.key()] = deepMerge(baseVal.toObject(), overlayVal.toObject());
-        else
-            result[it.key()] = overlayVal;
-    }
-    return result;
-}
-
-QString readUtf8(const QString &path, QString *error)
-{
-    QFile f(path);
-    if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
-        if (error)
-            *error = QStringLiteral("Cannot open: %1").arg(path);
-        return {};
-    }
-    return QString::fromUtf8(f.readAll());
-}
-
-std::optional parseTomlFile(const QString &path, QString *error)
-{
-    QString readErr;
-    const QString contents = readUtf8(path, &readErr);
-    if (!readErr.isEmpty()) {
-        if (error)
-            *error = readErr;
-        return std::nullopt;
-    }
-    toml::table tbl;
-    try {
-        tbl = toml::parse(contents.toStdString(), path.toStdString());
-    } catch (const toml::parse_error &e) {
-        std::ostringstream oss;
-        oss << e;
-        if (error) {
-            *error = QStringLiteral("TOML parse error in %1: %2")
-                         .arg(path, QString::fromStdString(oss.str()));
-        }
-        return std::nullopt;
-    }
-    return tomlToJson(tbl).toObject();
-}
-
-QStringList stringArray(const QJsonValue &v)
-{
-    QStringList out;
-    if (!v.isArray())
-        return out;
-    for (const auto &elem : v.toArray()) {
-        if (elem.isString())
-            out.append(elem.toString());
-    }
-    return out;
-}
-
-LaunchConfig launchConfigFromObject(const QJsonObject &launchObj)
-{
-    LaunchConfig l;
-    if (launchObj.isEmpty())
-        return l;
-    l.command = launchObj.value("command").toString();
-    l.args = stringArray(launchObj.value("args"));
-    l.cwd = launchObj.value("cwd").toString();
-    const QJsonObject envObj = launchObj.value("env").toObject();
-    for (auto it = envObj.constBegin(); it != envObj.constEnd(); ++it) {
-        if (it.value().isString())
-            l.env.insert(it.key(), it.value().toString());
-    }
-    l.readyUrl = launchObj.value("ready_url").toString();
-    if (launchObj.contains("ready_timeout_s")) {
-        const int raw = launchObj.value("ready_timeout_s")
-                            .toInt(static_cast(l.readyTimeout.count()));
-        l.readyTimeout = std::chrono::seconds{std::max(1, raw)};
-    }
-    l.autoStart = launchObj.value("auto_start").toBool(false);
-    l.detach = launchObj.value("detach").toBool(false);
-    return l;
-}
-
-ProviderInstance instanceFromMerged(const QJsonObject &obj)
-{
-    ProviderInstance inst;
-    inst.name        = obj.value("name").toString();
-    inst.clientApi   = obj.value("client_api").toString();
-    inst.description = obj.value("description").toString();
-    inst.url         = obj.value("url").toString();
-    inst.apiKeyRef   = obj.value("api_key_ref").toString();
-    inst.extras      = obj.value("extras").toObject();
-    inst.launch      = launchConfigFromObject(obj.value("launch").toObject());
-    inst.extendsName = obj.value("extends").toString();
-    inst.abstract    = obj.value("abstract").toBool(false);
-    return inst;
-}
-
-struct RawEntry
-{
-    QJsonObject obj;
-    QString filePath;
-    bool overridesBundled = false;
-};
-
-constexpr int kMaxExtendsDepth = 16;
-
-QJsonObject resolveExtends(
-    const QString &name,
-    const QHash &raw,
-    QSet &visiting,
-    QStringList &errors,
-    int depth = 0)
-{
-    if (depth > kMaxExtendsDepth) {
-        errors.append(QStringLiteral("Provider instance extends chain too deep (>%1) at '%2'")
-                          .arg(kMaxExtendsDepth)
-                          .arg(name));
-        return {};
-    }
-    if (visiting.contains(name)) {
-        errors.append(QStringLiteral("Cyclic 'extends' involving provider instance '%1'").arg(name));
-        return {};
-    }
-    if (!raw.contains(name)) {
-        errors.append(QStringLiteral("Unknown parent provider instance '%1'").arg(name));
-        return {};
-    }
-    visiting.insert(name);
-
-    QJsonObject self = raw.value(name).obj;
-    const QString parent = self.value("extends").toString();
-    if (!parent.isEmpty()) {
-        const QJsonObject parentMerged
-            = resolveExtends(parent, raw, visiting, errors, depth + 1);
-        self.remove("extends");
-        QJsonObject merged = deepMerge(parentMerged, self);
-        merged["name"] = name;
-        if (self.contains("abstract"))
-            merged["abstract"] = self.value("abstract");
-        else
-            merged.remove("abstract");
-        self = merged;
-    }
-    visiting.remove(name);
-    return self;
-}
-
-} // namespace
-
-std::optional ProviderInstanceLoader::parseFile(
-    const QString &path, QString *error)
-{
-    auto objOpt = parseTomlFile(path, error);
-    if (!objOpt)
-        return std::nullopt;
-    ProviderInstance inst = instanceFromMerged(*objOpt);
-    inst.sourcePath = path;
-    return inst;
-}
-
-ProviderInstanceLoader::LoadResult ProviderInstanceLoader::load(
-    const QString &qrcPrefix, const QString &userDir)
-{
-    LoadResult result;
-    QHash raw;
-
-    auto scan = [&](const QString &dir, bool isUserLayer) {
-        if (dir.isEmpty())
-            return;
-        QDir d(dir);
-        if (!d.exists())
-            return;
-        const QStringList files = d.entryList({"*.toml"}, QDir::Files);
-        for (const QString &fname : files) {
-            const QString fullPath = d.filePath(fname);
-            QString err;
-            auto objOpt = parseTomlFile(fullPath, &err);
-            if (!objOpt) {
-                result.errors.append(err);
-                continue;
-            }
-            const QString name = objOpt->value("name").toString();
-            if (name.isEmpty()) {
-                result.errors.append(
-                    QStringLiteral("Provider instance at %1 has no 'name'").arg(fullPath));
-                continue;
-            }
-            const bool overrides = isUserLayer && raw.contains(name);
-            raw.insert(name, {*objOpt, fullPath, overrides});
-        }
-    };
-
-    scan(qrcPrefix, /*isUserLayer=*/false);
-    scan(userDir,   /*isUserLayer=*/true);
-
-    for (auto it = raw.constBegin(); it != raw.constEnd(); ++it) {
-        const QString &name = it.key();
-
-        QSet visiting;
-        const QJsonObject merged = resolveExtends(name, raw, visiting, result.errors);
-        if (merged.isEmpty())
-            continue;
-
-        ProviderInstance inst = instanceFromMerged(merged);
-        inst.sourcePath = it.value().filePath;
-        inst.overridesBundled = it.value().overridesBundled;
-
-        if (inst.abstract)
-            continue;
-        result.instances.push_back(std::move(inst));
-    }
-    std::sort(result.instances.begin(), result.instances.end(),
-              [](const ProviderInstance &a, const ProviderInstance &b) {
-                  return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
-              });
-    return result;
-}
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderInstanceLoader.hpp b/sources/providersConfig/ProviderInstanceLoader.hpp
deleted file mode 100644
index 778b382..0000000
--- a/sources/providersConfig/ProviderInstanceLoader.hpp
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-
-#include 
-#include 
-
-#include "ProviderInstance.hpp"
-
-namespace QodeAssist::Providers {
-
-class ProviderInstanceLoader
-{
-public:
-    struct LoadResult
-    {
-        std::vector instances;
-        QStringList errors;
-        QStringList warnings;
-    };
-
-    static LoadResult load(const QString &qrcPrefix, const QString &userDir);
-
-    static std::optional parseFile(
-        const QString &path, QString *error);
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderInstanceWriter.cpp b/sources/providersConfig/ProviderInstanceWriter.cpp
deleted file mode 100644
index f5dbec0..0000000
--- a/sources/providersConfig/ProviderInstanceWriter.cpp
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "ProviderInstanceWriter.hpp"
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "ProviderInstanceFactory.hpp"
-#include "TomlWriter.hpp"
-
-namespace QodeAssist::Providers {
-
-namespace {
-
-struct Tr
-{
-    Q_DECLARE_TR_FUNCTIONS(QtC::QodeAssist)
-};
-
-constexpr int kIdentityKeyColumn = 11; // longest key: "description"
-constexpr int kLaunchKeyColumn = 15;   // longest key: "ready_timeout_s"
-
-void writeIdentityBlock(TomlSerializer::TomlWriter &w, const ProviderInstance &inst)
-{
-    w.setKeyColumnWidth(0);
-    w.writeInt(QStringLiteral("schema_version"), 1);
-    w.writeBlankLine();
-
-    w.setKeyColumnWidth(kIdentityKeyColumn);
-    w.writeString(QStringLiteral("name"), inst.name);
-    w.writeString(QStringLiteral("client_api"), inst.clientApi);
-    if (!inst.description.isEmpty())
-        w.writeString(QStringLiteral("description"), inst.description);
-    w.writeBlankLine();
-    w.writeString(QStringLiteral("url"), inst.url);
-    if (!inst.apiKeyRef.isEmpty())
-        w.writeString(QStringLiteral("api_key_ref"), inst.apiKeyRef);
-}
-
-void writeExtrasBlock(TomlSerializer::TomlWriter &w, const QJsonObject &extras)
-{
-    w.writeBlankLine();
-    w.writeTableHeader(QStringLiteral("extras"));
-    w.setKeyColumnWidth(0);
-    w.writeJsonPrimitives(extras);
-}
-
-void writeLaunchBlock(TomlSerializer::TomlWriter &w, const LaunchConfig &l)
-{
-    w.writeBlankLine();
-    w.writeTableHeader(QStringLiteral("launch"));
-    w.setKeyColumnWidth(kLaunchKeyColumn);
-    w.writeString(QStringLiteral("command"), l.command);
-    if (!l.args.isEmpty())
-        w.writeStringArray(QStringLiteral("args"), l.args);
-    if (!l.cwd.isEmpty())
-        w.writeString(QStringLiteral("cwd"), l.cwd);
-    if (!l.readyUrl.isEmpty())
-        w.writeString(QStringLiteral("ready_url"), l.readyUrl);
-    w.writeInt(QStringLiteral("ready_timeout_s"), l.readyTimeout.count());
-    w.writeBool(QStringLiteral("auto_start"), l.autoStart);
-    w.writeBool(QStringLiteral("detach"), l.detach);
-
-    if (!l.env.isEmpty()) {
-        w.writeBlankLine();
-        w.writeTableHeader(QStringLiteral("launch.env"));
-        w.setKeyColumnWidth(0);
-        w.writeStringDict(l.env);
-    }
-}
-
-} // namespace
-
-QString ProviderInstanceWriter::toToml(const ProviderInstance &inst)
-{
-    TomlSerializer::TomlWriter w;
-    writeIdentityBlock(w, inst);
-    if (!inst.extras.isEmpty())
-        writeExtrasBlock(w, inst.extras);
-    if (!inst.launch.isEmpty())
-        writeLaunchBlock(w, inst.launch);
-    return w.result();
-}
-
-QString ProviderInstanceWriter::deriveBaseName(const QString &name)
-{
-    QString baseName;
-    for (QChar c : name) {
-        if (c.isLetterOrNumber())
-            baseName.append(c.toLower());
-        else if (c == QLatin1Char(' ') || c == QLatin1Char('-') || c == QLatin1Char('_'))
-            baseName.append(QLatin1Char('_'));
-    }
-    while (baseName.startsWith(QLatin1Char('_')))
-        baseName.remove(0, 1);
-    while (baseName.endsWith(QLatin1Char('_')))
-        baseName.chop(1);
-    if (baseName.isEmpty())
-        baseName = QStringLiteral("instance");
-    return baseName;
-}
-
-namespace {
-constexpr int kMaxCollisionRetries = 1000;
-} // namespace
-
-QString ProviderInstanceWriter::pickUserFilePath(
-    const QString &userDir, const QString &name, const QString &previousPath)
-{
-    const QDir dir(userDir);
-    const QString base = deriveBaseName(name);
-    const QString preferred = dir.filePath(base + QLatin1String(".toml"));
-    if (!previousPath.isEmpty()
-        && QFileInfo(previousPath).absolutePath() == dir.absolutePath()
-        && QFileInfo(previousPath).absoluteFilePath() == QFileInfo(preferred).absoluteFilePath())
-        return preferred;
-    QSet taken;
-    for (const QString &existing : dir.entryList({"*.toml"}, QDir::Files))
-        taken.insert(existing);
-    if (!taken.contains(base + QLatin1String(".toml")))
-        return preferred;
-    for (int i = 2; i < kMaxCollisionRetries; ++i) {
-        const QString candidate = QStringLiteral("%1_%2.toml").arg(base).arg(i);
-        if (!taken.contains(candidate))
-            return dir.filePath(candidate);
-    }
-    return {};
-}
-
-QString ProviderInstanceWriter::writeToUserDir(
-    const ProviderInstance &inst, const QString &previousPath, QString *errorOut)
-{
-    const QString userDir = ProviderInstanceFactory::userInstancesDir();
-    if (!QDir().mkpath(userDir)) {
-        if (errorOut)
-            *errorOut = Tr::tr("Cannot create user provider folder:\n%1").arg(userDir);
-        return {};
-    }
-    const QString filePath = pickUserFilePath(userDir, inst.name, previousPath);
-    if (filePath.isEmpty()) {
-        if (errorOut)
-            *errorOut = Tr::tr("Cannot pick a free filename in:\n%1").arg(userDir);
-        return {};
-    }
-    QSaveFile f(filePath);
-    if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
-        if (errorOut)
-            *errorOut = Tr::tr("Cannot write %1:\n%2").arg(filePath, f.errorString());
-        return {};
-    }
-    const QByteArray bytes = toToml(inst).toUtf8();
-    if (f.write(bytes) != bytes.size() || !f.commit()) {
-        if (errorOut)
-            *errorOut = Tr::tr("Write failed for %1:\n%2").arg(filePath, f.errorString());
-        return {};
-    }
-    return filePath;
-}
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderInstanceWriter.hpp b/sources/providersConfig/ProviderInstanceWriter.hpp
deleted file mode 100644
index 7734d8e..0000000
--- a/sources/providersConfig/ProviderInstanceWriter.hpp
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "ProviderInstance.hpp"
-
-namespace QodeAssist::Providers {
-
-class ProviderInstanceWriter
-{
-public:
-    [[nodiscard]] static QString toToml(const ProviderInstance &inst);
-    [[nodiscard]] static QString writeToUserDir(
-        const ProviderInstance &inst,
-        const QString &previousPath,
-        QString *errorOut = nullptr);
-    [[nodiscard]] static QString pickUserFilePath(
-        const QString &userDir,
-        const QString &name,
-        const QString &previousPath);
-    [[nodiscard]] static QString deriveBaseName(const QString &name);
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderLauncher.cpp b/sources/providersConfig/ProviderLauncher.cpp
deleted file mode 100644
index d1231d3..0000000
--- a/sources/providersConfig/ProviderLauncher.cpp
+++ /dev/null
@@ -1,638 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "ProviderLauncher.hpp"
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "Logger.hpp"
-
-#include 
-#include 
-
-#ifdef Q_OS_WIN
-#include 
-#else
-#include 
-#endif
-
-namespace QodeAssist::Providers {
-
-namespace {
-
-Q_LOGGING_CATEGORY(launcherLog, "qodeassist.providerlauncher")
-
-constexpr std::chrono::milliseconds kProbeInterval{500};
-constexpr std::chrono::milliseconds kProbeTransferTimeout{2000};
-constexpr std::chrono::milliseconds kAdoptionTransferTimeout{1500};
-constexpr std::chrono::milliseconds kStartTimeout{2000};
-constexpr int kScrollbackBytesMax = 1 * 1024 * 1024; // 1 MiB cap per slot
-
-} // namespace
-
-struct ProviderLauncher::Slot
-{
-    QString name;
-    LaunchConfig cfg;
-    State state = Idle;
-    Utils::Process *process = nullptr;
-    qint64 detachedPid = 0;
-    bool adoptedExternal = false;
-    bool started = false;
-    QTimer *probeTimer = nullptr;
-    QTimer *startTimer = nullptr; // fail-fast timer for QProcess::started
-    QElapsedTimer probeStart;
-    QPointer probeReply;
-    QList> oneShotProbes;
-    int generation = 0;
-    QString lastError;
-    QByteArray scrollback;
-};
-
-ProviderLauncher::ProviderLauncher(QObject *parent)
-    : QObject(parent)
-    , m_nam(new QNetworkAccessManager(this))
-{
-    connect(m_nam, &QNetworkAccessManager::sslErrors, this,
-            [this](QNetworkReply *reply, const QList &errors) {
-                QStringList msgs;
-                msgs.reserve(errors.size());
-                for (const QSslError &e : errors)
-                    msgs.append(e.errorString());
-                qCWarning(launcherLog).noquote()
-                    << "SSL errors on probe to" << reply->url().toString() << ":"
-                    << msgs.join(QStringLiteral("; "));
-            });
-}
-
-ProviderLauncher::~ProviderLauncher()
-{
-    m_nam->disconnect(this);
-    for (Slot *slot : m_slots) {
-        if (slot->cfg.detach) {
-            if (slot->probeTimer) {
-                slot->probeTimer->stop();
-                slot->probeTimer->deleteLater();
-                slot->probeTimer = nullptr;
-            }
-            if (slot->probeReply) {
-                slot->probeReply->abort();
-                slot->probeReply->deleteLater();
-                slot->probeReply.clear();
-            }
-        } else {
-            teardownSlot(slot);
-        }
-        delete slot;
-    }
-    m_slots.clear();
-}
-
-void ProviderLauncher::start(const QString &instanceName, const LaunchConfig &cfg)
-{
-    if (instanceName.isEmpty() || cfg.isEmpty())
-        return;
-    Slot *slot = m_slots.value(instanceName, nullptr);
-    if (slot) {
-        if (slot->state == Starting || slot->state == Probing || slot->state == Ready) {
-            slot->cfg = cfg;
-            return;
-        }
-        teardownSlot(slot);
-    } else {
-        slot = new Slot;
-        slot->name = instanceName;
-        m_slots.insert(instanceName, slot);
-    }
-    slot->cfg = cfg;
-    slot->scrollback.clear();
-    slot->lastError.clear();
-    slot->detachedPid = 0;
-    slot->adoptedExternal = false;
-    slot->started = false;
-    ++slot->generation;
-    const int gen = slot->generation;
-
-    if (!cfg.readyUrl.isEmpty()) {
-        changeState(slot, Starting);
-        const QString name = instanceName;
-        const QString readyUrl = cfg.readyUrl;
-        probeOnceAsync(slot, gen, readyUrl, [this, name, readyUrl](bool ok) {
-            Slot *s = m_slots.value(name, nullptr);
-            if (!s || s->state != Starting)
-                return;
-            if (ok) {
-                s->adoptedExternal = true;
-                s->detachedPid = 0;
-                appendLog(s, QStringLiteral(
-                    "[adopt] %1 is already up — reusing the running process (no pid).")
-                              .arg(readyUrl));
-                changeState(s, Ready);
-                return;
-            }
-            if (s->cfg.detach)
-                launchDetached(s);
-            else
-                launchProcess(s);
-        });
-        return;
-    }
-
-    if (cfg.detach)
-        launchDetached(slot);
-    else
-        launchProcess(slot);
-}
-
-void ProviderLauncher::stop(const QString &instanceName)
-{
-    Slot *slot = m_slots.value(instanceName, nullptr);
-    if (!slot)
-        return;
-    if (slot->state == Idle || slot->state == Failed) {
-        changeState(slot, Idle);
-        return;
-    }
-    changeState(slot, Stopping);
-    if (slot->cfg.detach) {
-        const qint64 pid = slot->detachedPid;
-        const QString readyUrl = slot->cfg.readyUrl;
-        if (slot->probeTimer) {
-            slot->probeTimer->stop();
-            slot->probeTimer->deleteLater();
-            slot->probeTimer = nullptr;
-        }
-        if (slot->probeReply) {
-            slot->probeReply->abort();
-            slot->probeReply->deleteLater();
-            slot->probeReply.clear();
-        }
-        slot->detachedPid = 0;
-        slot->adoptedExternal = false;
-
-        if (pid <= 0) {
-            appendLog(slot, QStringLiteral(
-                "[stop] no pid recorded (process was adopted via probe) — "
-                "cannot terminate from the plugin; kill manually if needed."));
-            changeState(slot, Idle);
-            return;
-        }
-
-        if (readyUrl.isEmpty()) {
-            appendLog(slot, QStringLiteral("[stop] SIGTERM pid=%1").arg(pid));
-            killByPid(pid);
-            changeState(slot, Idle);
-            return;
-        }
-        const QString name = instanceName;
-        ++slot->generation;
-        const int gen = slot->generation;
-        probeOnceAsync(slot, gen, readyUrl, [this, name, pid](bool stillUp) {
-            Slot *s = m_slots.value(name, nullptr);
-            if (!s)
-                return;
-            if (stillUp) {
-                appendLog(s, QStringLiteral("[stop] SIGTERM pid=%1").arg(pid));
-                killByPid(pid);
-            } else {
-                appendLog(s, QStringLiteral(
-                    "[stop] pid=%1 no longer responsive on ready_url — "
-                    "skipping kill to avoid hitting a reused PID.").arg(pid));
-            }
-            if (s->state == Stopping)
-                changeState(s, Idle);
-        });
-        return;
-    }
-    teardownSlot(slot);
-    changeState(slot, Idle);
-}
-
-void ProviderLauncher::restart(const QString &instanceName, const LaunchConfig &cfg)
-{
-    stop(instanceName);
-    start(instanceName, cfg);
-}
-
-ProviderLauncher::State ProviderLauncher::state(const QString &instanceName) const
-{
-    const Slot *slot = m_slots.value(instanceName, nullptr);
-    return slot ? slot->state : Idle;
-}
-
-bool ProviderLauncher::isReady(const QString &instanceName) const
-{
-    return state(instanceName) == Ready;
-}
-
-QString ProviderLauncher::lastError(const QString &instanceName) const
-{
-    const Slot *slot = m_slots.value(instanceName, nullptr);
-    return slot ? slot->lastError : QString{};
-}
-
-QByteArray ProviderLauncher::scrollback(const QString &instanceName) const
-{
-    const Slot *slot = m_slots.value(instanceName, nullptr);
-    return slot ? slot->scrollback : QByteArray{};
-}
-
-QStringList ProviderLauncher::activeInstances() const
-{
-    QStringList out;
-    for (auto it = m_slots.constBegin(); it != m_slots.constEnd(); ++it) {
-        if (it.value()->state != Idle)
-            out.append(it.key());
-    }
-    std::sort(out.begin(), out.end(),
-              [](const QString &a, const QString &b) {
-                  return a.compare(b, Qt::CaseInsensitive) < 0;
-              });
-    return out;
-}
-
-void ProviderLauncher::launchProcess(Slot *slot)
-{
-    const LaunchConfig &cfg = slot->cfg;
-    const QString command = expandVars(cfg.command, slot);
-    const QStringList args = expandVars(cfg.args, slot);
-    const QString cwd = cfg.cwd.isEmpty() ? QDir::homePath() : expandVars(cfg.cwd, slot);
-    const QString name = slot->name;
-
-    auto *proc = new Utils::Process(this);
-    slot->process = proc;
-
-    Utils::Environment env = Utils::Environment::systemEnvironment();
-    env.set(QStringLiteral("PROVIDER_NAME"), slot->name);
-    for (auto it = cfg.env.constBegin(); it != cfg.env.constEnd(); ++it)
-        env.set(it.key(), it.value());
-    proc->setEnvironment(env);
-    proc->setWorkingDirectory(Utils::FilePath::fromString(cwd));
-    proc->setCommand(Utils::CommandLine{Utils::FilePath::fromString(command), args});
-
-    proc->setPtyData(Utils::Pty::Data{});
-
-    connect(proc, &Utils::Process::readyReadStandardOutput, this, [this, name] {
-        Slot *s = m_slots.value(name, nullptr);
-        if (!s || !s->process) return;
-        const QByteArray chunk = s->process->readAllRawStandardOutput();
-        if (!chunk.isEmpty()) {
-            appendScrollback(s, chunk);
-            emit bytesReceived(s->name, chunk);
-        }
-    });
-    connect(proc, &Utils::Process::readyReadStandardError, this, [this, name] {
-        Slot *s = m_slots.value(name, nullptr);
-        if (!s || !s->process) return;
-        const QByteArray chunk = s->process->readAllRawStandardError();
-        if (!chunk.isEmpty()) {
-            appendScrollback(s, chunk);
-            emit bytesReceived(s->name, chunk);
-        }
-    });
-    connect(proc, &Utils::Process::started, this, [this, name] {
-        Slot *s = m_slots.value(name, nullptr);
-        if (!s)
-            return;
-        s->started = true;
-        if (s->startTimer) {
-            s->startTimer->stop();
-            s->startTimer->deleteLater();
-            s->startTimer = nullptr;
-        }
-        if (s->state != Starting)
-            return;
-        if (s->cfg.readyUrl.isEmpty()) {
-            changeState(s, Ready);
-            return;
-        }
-        s->probeStart.start();
-        changeState(s, Probing);
-        scheduleReadyProbe(s);
-    });
-
-    connect(proc, &Utils::Process::done, this, [this, name] {
-        Slot *s = m_slots.value(name, nullptr);
-        if (!s || !s->process) return;
-        const QByteArray tailOut = s->process->readAllRawStandardOutput();
-        const QByteArray tailErr = s->process->readAllRawStandardError();
-        if (!tailOut.isEmpty()) { appendScrollback(s, tailOut); emit bytesReceived(s->name, tailOut); }
-        if (!tailErr.isEmpty()) { appendScrollback(s, tailErr); emit bytesReceived(s->name, tailErr); }
-
-        const int code = s->process->exitCode();
-        const QProcess::ExitStatus status = s->process->exitStatus();
-        appendLog(s, QStringLiteral("[exit] code=%1 status=%2")
-                            .arg(code)
-                            .arg(status == QProcess::NormalExit ? "normal" : "crashed"));
-        const State prev = s->state;
-        teardownSlot(s);
-        if (prev != Stopping && code != 0) {
-            s->lastError = QStringLiteral("Process exited (code %1)").arg(code);
-            changeState(s, Failed);
-        } else {
-            changeState(s, Idle);
-        }
-    });
-
-    appendLog(slot, QStringLiteral("[spawn] %1 %2")
-                        .arg(command, args.join(QLatin1Char(' '))));
-    changeState(slot, Starting);
-    proc->start();
-
-    if (slot->startTimer) {
-        slot->startTimer->stop();
-        slot->startTimer->deleteLater();
-    }
-    slot->startTimer = new QTimer(this);
-    slot->startTimer->setSingleShot(true);
-    const QString slotName = slot->name;
-    connect(slot->startTimer, &QTimer::timeout, this, [this, slotName] {
-        Slot *s = m_slots.value(slotName, nullptr);
-        if (!s || s->started || s->state != Starting)
-            return;
-        s->lastError = s->process && !s->process->errorString().isEmpty()
-                           ? s->process->errorString()
-                           : QStringLiteral("Process failed to start");
-        appendLog(s, QStringLiteral("[error] %1").arg(s->lastError));
-        teardownSlot(s);
-        changeState(s, Failed);
-    });
-    slot->startTimer->start(kStartTimeout);
-}
-
-void ProviderLauncher::launchDetached(Slot *slot)
-{
-    const LaunchConfig &cfg = slot->cfg;
-    const QString command = expandVars(cfg.command, slot);
-    const QStringList args = expandVars(cfg.args, slot);
-    const QString cwd = cfg.cwd.isEmpty() ? QDir::homePath() : expandVars(cfg.cwd, slot);
-
-    appendLog(slot, QStringLiteral("[spawn-detached] %1 %2")
-                        .arg(command, args.join(QLatin1Char(' '))));
-
-    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
-    env.insert(QStringLiteral("PROVIDER_NAME"), slot->name);
-    for (auto it = cfg.env.constBegin(); it != cfg.env.constEnd(); ++it)
-        env.insert(it.key(), it.value());
-
-    QProcess tmp;
-    tmp.setProgram(command);
-    tmp.setArguments(args);
-    tmp.setWorkingDirectory(cwd);
-    tmp.setProcessEnvironment(env);
-    tmp.setStandardOutputFile(QProcess::nullDevice());
-    tmp.setStandardErrorFile(QProcess::nullDevice());
-    qint64 pid = 0;
-    const bool ok = tmp.startDetached(&pid);
-    if (!ok || pid <= 0) {
-        slot->lastError = tmp.errorString().isEmpty()
-                              ? QStringLiteral("Detached spawn failed")
-                              : tmp.errorString();
-        appendLog(slot, QStringLiteral("[error] %1").arg(slot->lastError));
-        changeState(slot, Failed);
-        return;
-    }
-    slot->detachedPid = pid;
-    appendLog(slot, QStringLiteral("[detached] pid=%1 (stdout/stderr discarded)").arg(pid));
-
-    if (cfg.readyUrl.isEmpty()) {
-        changeState(slot, Ready);
-        return;
-    }
-    slot->probeStart.start();
-    changeState(slot, Probing);
-    scheduleReadyProbe(slot);
-}
-
-void ProviderLauncher::probeOnceAsync(
-    Slot *slot, int expectedGeneration, const QString &url,
-    std::function onResult)
-{
-    QNetworkRequest req(QUrl{url});
-    req.setTransferTimeout(kAdoptionTransferTimeout);
-    QNetworkReply *reply = m_nam->get(req);
-    if (slot)
-        slot->oneShotProbes.append(QPointer(reply));
-    const QString name = slot ? slot->name : QString{};
-    connect(reply, &QNetworkReply::finished, this,
-            [this, reply, name, expectedGeneration, cb = std::move(onResult)] {
-                reply->deleteLater();
-                Slot *s = m_slots.value(name, nullptr);
-                if (s) {
-                    s->oneShotProbes.removeAll(QPointer(reply));
-                    if (s->generation != expectedGeneration)
-                        return;
-                }
-                const int http = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-                const bool ok = reply->error() == QNetworkReply::NoError && http >= 200 && http < 300;
-                cb(ok);
-            });
-}
-
-void ProviderLauncher::killByPid(qint64 pid)
-{
-    if (pid <= 0)
-        return;
-#ifdef Q_OS_WIN
-    HANDLE h = ::OpenProcess(PROCESS_TERMINATE, FALSE, static_cast(pid));
-    if (h) {
-        ::TerminateProcess(h, 1);
-        ::CloseHandle(h);
-    }
-#else
-    ::kill(static_cast(pid), SIGTERM);
-#endif
-}
-
-void ProviderLauncher::scheduleReadyProbe(Slot *slot)
-{
-    if (!slot->probeTimer) {
-        const QString name = slot->name;
-        slot->probeTimer = new QTimer(this);
-        slot->probeTimer->setSingleShot(true);
-        connect(slot->probeTimer, &QTimer::timeout, this, [this, name] {
-            if (Slot *s = m_slots.value(name, nullptr))
-                runReadyProbe(s);
-        });
-    }
-    slot->probeTimer->start(kProbeInterval);
-}
-
-void ProviderLauncher::runReadyProbe(Slot *slot)
-{
-    if (!slot || slot->state != Probing)
-        return;
-    const auto elapsed = std::chrono::milliseconds{slot->probeStart.elapsed()};
-    if (elapsed > slot->cfg.readyTimeout) {
-        slot->lastError = QStringLiteral("Ready probe timed out after %1 s")
-                              .arg(slot->cfg.readyTimeout.count());
-        appendLog(slot, QStringLiteral("[probe] timeout — %1").arg(slot->lastError));
-        changeState(slot, Failed);
-        teardownSlot(slot);
-        return;
-    }
-    QNetworkRequest req(QUrl{slot->cfg.readyUrl});
-    req.setTransferTimeout(kProbeTransferTimeout);
-    slot->probeReply = m_nam->get(req);
-    const QString name = slot->name;
-    connect(slot->probeReply, &QNetworkReply::finished, this, [this, name] {
-        Slot *s = m_slots.value(name, nullptr);
-        if (!s || !s->probeReply)
-            return;
-        QNetworkReply *reply = s->probeReply;
-        s->probeReply.clear();
-        reply->deleteLater();
-        if (s->state != Probing)
-            return;
-        const int http = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
-        if (reply->error() == QNetworkReply::NoError && http >= 200 && http < 300) {
-            appendLog(s, QStringLiteral("[probe] %1 → %2 OK").arg(s->cfg.readyUrl).arg(http));
-            changeState(s, Ready);
-            return;
-        }
-        if (reply->error() != QNetworkReply::NoError) {
-            appendLog(s, QStringLiteral("[probe] %1 → %2")
-                              .arg(s->cfg.readyUrl, reply->errorString()));
-        }
-        scheduleReadyProbe(s);
-    });
-}
-
-void ProviderLauncher::teardownSlot(Slot *slot)
-{
-    if (!slot)
-        return;
-
-    ++slot->generation;
-    if (slot->probeTimer) {
-        slot->probeTimer->stop();
-        slot->probeTimer->deleteLater();
-        slot->probeTimer = nullptr;
-    }
-    if (slot->startTimer) {
-        slot->startTimer->stop();
-        slot->startTimer->deleteLater();
-        slot->startTimer = nullptr;
-    }
-    if (slot->probeReply) {
-        slot->probeReply->abort();
-        slot->probeReply->deleteLater();
-        slot->probeReply.clear();
-    }
-    for (const QPointer &probe : slot->oneShotProbes) {
-        if (probe) {
-            probe->abort();
-            probe->deleteLater();
-        }
-    }
-    slot->oneShotProbes.clear();
-    if (slot->process) {
-        Utils::Process *p = slot->process;
-        slot->process = nullptr;
-        p->disconnect(this);
-        if (p->state() == QProcess::NotRunning) {
-            p->deleteLater();
-        } else {
-            QObject::connect(p, &Utils::Process::done, p, &QObject::deleteLater);
-            QTimer::singleShot(std::chrono::seconds{15}, p, &QObject::deleteLater);
-            p->stop();
-        }
-    }
-}
-
-void ProviderLauncher::appendLog(Slot *slot, const QString &line)
-{
-    if (line.isEmpty())
-        return;
-    const QByteArray bytes = (line + QStringLiteral("\r\n")).toUtf8();
-    appendScrollback(slot, bytes);
-    emit bytesReceived(slot->name, bytes);
-}
-
-void ProviderLauncher::appendScrollback(Slot *slot, const QByteArray &chunk)
-{
-    if (chunk.isEmpty())
-        return;
-    slot->scrollback.append(chunk);
-    if (slot->scrollback.size() > kScrollbackBytesMax) {
-        const int over = slot->scrollback.size() - kScrollbackBytesMax;
-        slot->scrollback.remove(0, over);
-    }
-}
-
-void ProviderLauncher::changeState(Slot *slot, State newState)
-{
-    if (slot->state == newState)
-        return;
-    slot->state = newState;
-    const QString name = slot->name;
-    qCDebug(launcherLog).noquote() << name << "→ state" << newState;
-    emit stateChanged(name, newState);
-}
-
-QString ProviderLauncher::expandOne(
-    const QString &input, const Slot *slot, const QProcessEnvironment &sys)
-{
-    if (!input.contains(QLatin1String("${")))
-        return input;
-    QString out = input;
-    int searchFrom = 0;
-    while (searchFrom < out.size()) {
-        const int open = out.indexOf(QLatin1String("${"), searchFrom);
-        if (open < 0)
-            break;
-        const int close = out.indexOf(QLatin1Char('}'), open + 2);
-        if (close < 0)
-            break;
-        const QString key = out.mid(open + 2, close - open - 2);
-        QString value;
-        if (slot && slot->cfg.env.contains(key))
-            value = slot->cfg.env.value(key);
-        else if (key == QLatin1String("PROVIDER_NAME") && slot)
-            value = slot->name;
-        else if (sys.contains(key))
-            value = sys.value(key);
-        out.replace(open, close - open + 1, value);
-        searchFrom = open + value.size();
-    }
-    return out;
-}
-
-QString ProviderLauncher::expandVars(const QString &input, const Slot *slot) const
-{
-    if (!input.contains(QLatin1String("${")))
-        return input;
-    return expandOne(input, slot, QProcessEnvironment::systemEnvironment());
-}
-
-QStringList ProviderLauncher::expandVars(const QStringList &args, const Slot *slot) const
-{
-    if (args.isEmpty())
-        return {};
-
-    const QProcessEnvironment sys = QProcessEnvironment::systemEnvironment();
-    QStringList out;
-    out.reserve(args.size());
-    for (const QString &a : args)
-        out.append(expandOne(a, slot, sys));
-    return out;
-}
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderLauncher.hpp b/sources/providersConfig/ProviderLauncher.hpp
deleted file mode 100644
index 371de40..0000000
--- a/sources/providersConfig/ProviderLauncher.hpp
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "ProviderInstance.hpp"
-
-class QNetworkAccessManager;
-class QNetworkReply;
-class QProcessEnvironment;
-class QTimer;
-
-namespace Utils { class Process; }
-
-namespace QodeAssist::Providers {
-
-class ProviderLauncher : public QObject
-{
-    Q_OBJECT
-    Q_DISABLE_COPY_MOVE(ProviderLauncher)
-public:
-    enum State : quint8 {
-        Idle,      // no process running
-        Starting,  // QProcess::start invoked, waiting for it to be alive
-        Probing,   // process alive, polling ready_url
-        Ready,     // ready probe succeeded (or no probe configured)
-        Stopping,  // termination requested; waiting for QProcess to exit
-        Failed,    // process exited unexpectedly or readiness probe gave up
-    };
-    Q_ENUM(State)
-
-    explicit ProviderLauncher(QObject *parent = nullptr);
-    ~ProviderLauncher() override;
-
-
-    void start(const QString &instanceName, const LaunchConfig &cfg);
-    void stop(const QString &instanceName);
-    void restart(const QString &instanceName, const LaunchConfig &cfg);
-
-    [[nodiscard]] State state(const QString &instanceName) const;
-    [[nodiscard]] bool isReady(const QString &instanceName) const;
-    [[nodiscard]] QString lastError(const QString &instanceName) const;
-    [[nodiscard]] QByteArray scrollback(const QString &instanceName) const;
-
-    [[nodiscard]] QStringList activeInstances() const;
-
-signals:
-    void stateChanged(const QString &instanceName, State newState);
-    void bytesReceived(const QString &instanceName, const QByteArray &chunk);
-
-private:
-    struct Slot;
-    void teardownSlot(Slot *slot);
-    void launchProcess(Slot *slot);
-    void launchDetached(Slot *slot);
-    void appendScrollback(Slot *slot, const QByteArray &chunk);
-    void scheduleReadyProbe(Slot *slot);
-    void runReadyProbe(Slot *slot);
-    void probeOnceAsync(Slot *slot, int expectedGeneration, const QString &url,
-                        std::function onResult);
-    void appendLog(Slot *slot, const QString &line);
-    void changeState(Slot *slot, State newState);
-    QString expandVars(const QString &input, const Slot *slot) const;
-    QStringList expandVars(const QStringList &args, const Slot *slot) const;
-    static QString expandOne(const QString &input, const Slot *slot,
-                             const QProcessEnvironment &sys);
-    static void killByPid(qint64 pid);
-
-    QHash m_slots;
-    QNetworkAccessManager *m_nam = nullptr;
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderSecretsStore.cpp b/sources/providersConfig/ProviderSecretsStore.cpp
deleted file mode 100644
index ff06200..0000000
--- a/sources/providersConfig/ProviderSecretsStore.cpp
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "ProviderSecretsStore.hpp"
-
-#include 
-#include 
-
-namespace QodeAssist::Providers {
-
-namespace {
-
-constexpr auto kGroup = "QodeAssist/Keychain";
-
-Utils::Key settingsKey(const QString &ref)
-{
-    return Utils::Key(QStringLiteral("%1/%2")
-                          .arg(QLatin1StringView(kGroup))
-                          .arg(ref)
-                          .toUtf8());
-}
-
-} // namespace
-
-ProviderSecretsStore::ProviderSecretsStore(QObject *parent)
-    : QObject(parent)
-{}
-
-ProviderSecretsStore::~ProviderSecretsStore() = default;
-
-QString ProviderSecretsStore::readKeySync(const QString &ref) const
-{
-    if (ref.isEmpty())
-        return {};
-    auto *s = Core::ICore::settings();
-    if (!s)
-        return {};
-    return s->value(settingsKey(ref)).toString();
-}
-
-void ProviderSecretsStore::writeKey(const QString &ref, const QString &value)
-{
-    if (ref.isEmpty())
-        return;
-    auto *s = Core::ICore::settings();
-    if (!s)
-        return;
-    s->setValue(settingsKey(ref), value);
-    s->sync();
-    emit keyChanged(ref);
-}
-
-void ProviderSecretsStore::eraseKey(const QString &ref)
-{
-    if (ref.isEmpty())
-        return;
-    auto *s = Core::ICore::settings();
-    if (!s)
-        return;
-    s->remove(settingsKey(ref));
-    s->sync();
-    emit keyChanged(ref);
-}
-
-bool ProviderSecretsStore::hasKey(const QString &ref) const
-{
-    if (ref.isEmpty())
-        return false;
-    auto *s = Core::ICore::settings();
-    if (!s)
-        return false;
-    return s->contains(settingsKey(ref));
-}
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/ProviderSecretsStore.hpp b/sources/providersConfig/ProviderSecretsStore.hpp
deleted file mode 100644
index f0538e4..0000000
--- a/sources/providersConfig/ProviderSecretsStore.hpp
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-
-namespace QodeAssist::Providers {
-
-class ProviderSecretsStore : public QObject
-{
-    Q_OBJECT
-    Q_DISABLE_COPY_MOVE(ProviderSecretsStore)
-public:
-    explicit ProviderSecretsStore(QObject *parent = nullptr);
-    ~ProviderSecretsStore() override;
-
-    [[nodiscard]] QString readKeySync(const QString &ref) const;
-
-    void writeKey(const QString &ref, const QString &value);
-    void eraseKey(const QString &ref);
-
-    [[nodiscard]] bool hasKey(const QString &ref) const;
-
-signals:
-    void keyChanged(const QString &ref);
-};
-
-} // namespace QodeAssist::Providers
diff --git a/sources/providersConfig/claude.toml b/sources/providersConfig/claude.toml
deleted file mode 100644
index fd6eac1..0000000
--- a/sources/providersConfig/claude.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "Claude"
-client_api  = "Claude"
-description = "Anthropic's hosted Claude API."
-
-url         = "https://api.anthropic.com"
-api_key_ref = "qodeassist/providers/Claude"
diff --git a/sources/providersConfig/codestral.toml b/sources/providersConfig/codestral.toml
deleted file mode 100644
index 2f4c75b..0000000
--- a/sources/providersConfig/codestral.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "Codestral"
-client_api  = "Codestral"
-description = "Mistral's Codestral FIM-capable code model API."
-
-url         = "https://codestral.mistral.ai"
-api_key_ref = "qodeassist/providers/Codestral"
diff --git a/sources/providersConfig/googleai.toml b/sources/providersConfig/googleai.toml
deleted file mode 100644
index 4ae0f06..0000000
--- a/sources/providersConfig/googleai.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "Google AI"
-client_api  = "Google AI"
-description = "Google AI Studio (Gemini) hosted API."
-
-url         = "https://generativelanguage.googleapis.com/v1beta"
-api_key_ref = "qodeassist/providers/Google AI"
diff --git a/sources/providersConfig/llamacpp.toml b/sources/providersConfig/llamacpp.toml
deleted file mode 100644
index 3764ae4..0000000
--- a/sources/providersConfig/llamacpp.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "llama.cpp"
-client_api  = "llama.cpp"
-description = "Local llama.cpp server (llama-server)."
-
-url         = "http://localhost:8080"
-api_key_ref = "qodeassist/providers/llama.cpp"
diff --git a/sources/providersConfig/lmstudio_chat.toml b/sources/providersConfig/lmstudio_chat.toml
deleted file mode 100644
index 672ed30..0000000
--- a/sources/providersConfig/lmstudio_chat.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "LM Studio (Chat Completions)"
-client_api  = "LM Studio (Chat Completions)"
-description = "Local LM Studio server over the /v1/chat/completions endpoint."
-
-url         = "http://localhost:1234"
-api_key_ref = "qodeassist/providers/LM Studio (Chat Completions)"
diff --git a/sources/providersConfig/lmstudio_responses.toml b/sources/providersConfig/lmstudio_responses.toml
deleted file mode 100644
index e2e46fb..0000000
--- a/sources/providersConfig/lmstudio_responses.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "LM Studio (Responses API)"
-client_api  = "LM Studio (Responses API)"
-description = "Local LM Studio server over the /v1/responses endpoint."
-
-url         = "http://localhost:1234"
-api_key_ref = "qodeassist/providers/LM Studio (Responses API)"
diff --git a/sources/providersConfig/mistral.toml b/sources/providersConfig/mistral.toml
deleted file mode 100644
index 4ca6f25..0000000
--- a/sources/providersConfig/mistral.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "Mistral AI"
-client_api  = "Mistral AI"
-description = "Mistral's hosted chat / completions API."
-
-url         = "https://api.mistral.ai"
-api_key_ref = "qodeassist/providers/Mistral AI"
diff --git a/sources/providersConfig/ollama_compat.toml b/sources/providersConfig/ollama_compat.toml
deleted file mode 100644
index 998053f..0000000
--- a/sources/providersConfig/ollama_compat.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "Ollama (OpenAI-compatible)"
-client_api  = "Ollama (OpenAI-compatible)"
-description = "Local Ollama daemon spoken to via the OpenAI-compatible /v1 routes."
-
-url         = "http://localhost:11434"
-api_key_ref = "qodeassist/providers/Ollama (OpenAI-compatible)"
diff --git a/sources/providersConfig/ollama_native.toml b/sources/providersConfig/ollama_native.toml
deleted file mode 100644
index 4e900e7..0000000
--- a/sources/providersConfig/ollama_native.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "Ollama (Native)"
-client_api  = "Ollama (Native)"
-description = "Default local Ollama daemon over its native /api/* endpoints."
-
-url         = "http://localhost:11434"
-api_key_ref = "qodeassist/providers/Ollama (Native)"
diff --git a/sources/providersConfig/openai_chat.toml b/sources/providersConfig/openai_chat.toml
deleted file mode 100644
index da0be10..0000000
--- a/sources/providersConfig/openai_chat.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "OpenAI (Chat Completions)"
-client_api  = "OpenAI (Chat Completions)"
-description = "OpenAI's hosted /v1/chat/completions endpoint."
-
-url         = "https://api.openai.com/v1"
-api_key_ref = "qodeassist/providers/OpenAI (Chat Completions)"
diff --git a/sources/providersConfig/openai_compat.toml b/sources/providersConfig/openai_compat.toml
deleted file mode 100644
index b81665b..0000000
--- a/sources/providersConfig/openai_compat.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "OpenAI Compatible"
-client_api  = "OpenAI Compatible"
-description = "Self-hosted OpenAI-compatible server (vLLM, TGI, ...). Edit the URL to match your deployment."
-
-url         = "http://localhost:1234/v1"
-api_key_ref = "qodeassist/providers/OpenAI Compatible"
diff --git a/sources/providersConfig/openai_responses.toml b/sources/providersConfig/openai_responses.toml
deleted file mode 100644
index 2fa3a3c..0000000
--- a/sources/providersConfig/openai_responses.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "OpenAI (Responses API)"
-client_api  = "OpenAI (Responses API)"
-description = "OpenAI's hosted /v1/responses endpoint."
-
-url         = "https://api.openai.com/v1"
-api_key_ref = "qodeassist/providers/OpenAI (Responses API)"
diff --git a/sources/providersConfig/openrouter.toml b/sources/providersConfig/openrouter.toml
deleted file mode 100644
index d7753f1..0000000
--- a/sources/providersConfig/openrouter.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-schema_version = 1
-
-name        = "OpenRouter"
-client_api  = "OpenRouter"
-description = "OpenRouter aggregator (https://openrouter.ai)."
-
-url         = "https://openrouter.ai/api"
-api_key_ref = "qodeassist/providers/OpenRouter"
diff --git a/sources/providersConfig/provider_instances.qrc b/sources/providersConfig/provider_instances.qrc
deleted file mode 100644
index aa8a855..0000000
--- a/sources/providersConfig/provider_instances.qrc
+++ /dev/null
@@ -1,17 +0,0 @@
-
-    
-        ollama_native.toml
-        ollama_compat.toml
-        claude.toml
-        openai_chat.toml
-        openai_responses.toml
-        openai_compat.toml
-        lmstudio_chat.toml
-        lmstudio_responses.toml
-        openrouter.toml
-        mistral.toml
-        codestral.toml
-        googleai.toml
-        llamacpp.toml
-    
-
diff --git a/sources/settings/AgentPipelinesPage.cpp b/sources/settings/AgentPipelinesPage.cpp
deleted file mode 100644
index 31400d1..0000000
--- a/sources/settings/AgentPipelinesPage.cpp
+++ /dev/null
@@ -1,274 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "AgentPipelinesPage.hpp"
-
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "../../Version.hpp"
-#include "AgentRosterWidget.hpp"
-#include "AgentsSettingsPage.hpp"
-#include "Logger.hpp"
-#include "PipelinesConfig.hpp"
-#include "SettingsConstants.hpp"
-#include "SettingsTr.hpp"
-
-#include 
-
-namespace QodeAssist::Settings {
-
-AgentPipelinesPageNavigator::AgentPipelinesPageNavigator(QObject *parent)
-    : QObject(parent)
-{}
-
-namespace {
-
-constexpr int kSaveDebounceMs = 300;
-
-struct SlotMeta
-{
-    const char *title;
-    const char *hint;
-};
-
-const SlotMeta kSlotMeta[] = {
-    {TrConstants::CODE_COMPLETION,  TrConstants::SLOT_HINT_CODE_COMPLETION},
-    {TrConstants::CHAT_ASSISTANT,   TrConstants::SLOT_HINT_CHAT_ASSISTANT},
-    {TrConstants::CHAT_COMPRESSION, TrConstants::SLOT_HINT_CHAT_COMPRESSION},
-    {TrConstants::QUICK_REFACTOR,   TrConstants::SLOT_HINT_QUICK_REFACTOR},
-};
-
-class AgentPipelinesPageWidget : public Core::IOptionsPageWidget
-{
-    Q_OBJECT
-    Q_DISABLE_COPY_MOVE(AgentPipelinesPageWidget)
-public:
-    AgentPipelinesPageWidget(
-        const QPointer &agentFactory,
-        const QPointer &navigator,
-        const QPointer &agentsNavigator)
-        : m_agentFactory(agentFactory)
-        , m_navigator(navigator)
-        , m_agentsNavigator(agentsNavigator)
-    {
-        m_titleLabel = new QLabel(Tr::tr(TrConstants::AGENT_PIPELINES), this);
-        QFont tf = m_titleLabel->font();
-        tf.setBold(true);
-        tf.setPixelSize(13);
-        m_titleLabel->setFont(tf);
-
-        m_resetBtn = new QPushButton(Tr::tr(TrConstants::RESET_TO_DEFAULTS), this);
-
-        auto *headerRow = new QHBoxLayout;
-        headerRow->setContentsMargins(0, 0, 0, 0);
-        headerRow->setSpacing(8);
-        headerRow->addWidget(m_titleLabel);
-        headerRow->addStretch(1);
-        headerRow->addWidget(m_resetBtn);
-
-        auto *headerSep = new QFrame(this);
-        headerSep->setFrameShape(QFrame::HLine);
-        headerSep->setFrameShadow(QFrame::Sunken);
-
-        m_rosters[0] = new AgentRosterWidget(this);
-        m_rosters[1] = new AgentRosterWidget(this);
-        m_rosters[2] = new AgentRosterWidget(this);
-        m_rosters[3] = new AgentRosterWidget(this);
-
-        for (int i = 0; i < kRosterCount; ++i)
-            m_rosters[i]->setSlot(Tr::tr(kSlotMeta[i].title), Tr::tr(kSlotMeta[i].hint), {});
-
-        auto *content = new QWidget(this);
-        auto *contentLay = new QVBoxLayout(content);
-        contentLay->setContentsMargins(0, 0, 0, 0);
-        contentLay->setSpacing(12);
-        for (int i = 0; i < kRosterCount; ++i)
-            contentLay->addWidget(m_rosters[i]);
-        contentLay->addStretch(1);
-
-        auto *scroll = new QScrollArea(this);
-        scroll->setWidgetResizable(true);
-        scroll->setFrameShape(QFrame::NoFrame);
-        scroll->setWidget(content);
-
-        auto *root = new QVBoxLayout(this);
-        root->setContentsMargins(8, 8, 8, 8);
-        root->setSpacing(6);
-        root->addLayout(headerRow);
-        root->addWidget(headerSep);
-        root->addWidget(scroll, 1);
-
-        m_saveDebounce = new QTimer(this);
-        m_saveDebounce->setSingleShot(true);
-        m_saveDebounce->setInterval(kSaveDebounceMs);
-        connect(m_saveDebounce, &QTimer::timeout, this, [this]() { persistRosters(); });
-
-        loadFromSettings();
-
-        connect(m_resetBtn, &QPushButton::clicked, this, &AgentPipelinesPageWidget::onReset);
-
-        for (int i = 0; i < kRosterCount; ++i) {
-            connect(m_rosters[i], &AgentRosterWidget::editAgentRequested, this,
-                    &AgentPipelinesPageWidget::onEditAgent);
-            connect(m_rosters[i], &AgentRosterWidget::rosterChanged, this,
-                    [this](const QStringList &) { m_saveDebounce->start(); });
-        }
-    }
-
-    ~AgentPipelinesPageWidget() override
-    {
-        if (m_saveDebounce && m_saveDebounce->isActive()) {
-            m_saveDebounce->stop();
-            persistRosters();
-        }
-    }
-
-    void apply() final
-    {
-        if (m_saveDebounce && m_saveDebounce->isActive())
-            m_saveDebounce->stop();
-        persistRosters();
-    }
-
-private:
-    static constexpr int kRosterCount = 4;
-
-    void persistRosters()
-    {
-        PipelineRosters rosters;
-        rosters.codeCompletion = m_rosters[0]->roster();
-        rosters.chatAssistant = m_rosters[1]->roster();
-        rosters.chatCompression = m_rosters[2]->roster();
-        rosters.quickRefactor = m_rosters[3]->roster();
-        QString err;
-        if (!PipelinesConfig::save(rosters, &err)) {
-            LOG_MESSAGE(QStringLiteral("[Pipelines] save failed (%1): %2")
-                            .arg(PipelinesConfig::filePath(), err));
-            if (!m_saveErrorShown) {
-                m_saveErrorShown = true;
-                QMessageBox::warning(
-                    Core::ICore::dialogParent(),
-                    Tr::tr(TrConstants::AGENT_PIPELINES),
-                    tr("Failed to save pipelines.toml:\n%1\n\n"
-                       "Further save failures will only be logged.")
-                        .arg(err));
-            }
-        } else {
-            m_saveErrorShown = false;
-        }
-    }
-
-    void onReset()
-    {
-        const auto reply = QMessageBox::question(
-            Core::ICore::dialogParent(),
-            Tr::tr(TrConstants::RESET_SETTINGS),
-            Tr::tr(TrConstants::CONFIRMATION),
-            QMessageBox::Yes | QMessageBox::No);
-
-        if (reply != QMessageBox::Yes)
-            return;
-
-        QString err;
-        if (!PipelinesConfig::save(PipelineRosters::defaults(), &err))
-            LOG_MESSAGE(QStringLiteral("[Pipelines] failed to reset rosters: %1").arg(err));
-
-        m_saveErrorShown = false;
-        loadFromSettings();
-    }
-
-    void onEditAgent(const QString &name)
-    {
-        if (m_agentsNavigator)
-            m_agentsNavigator->requestSelectAgent(name);
-        if (m_navigator)
-            emit m_navigator->editAgentRequested(name);
-
-#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
-        Core::ICore::showSettings(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
-#else
-        Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_AGENTS_SETTINGS_PAGE_ID);
-#endif
-    }
-
-    void loadFromSettings()
-    {
-        const PipelinesLoadResult lr = PipelinesConfig::load();
-        if (lr.status == PipelinesLoadStatus::ParseError
-            || lr.status == PipelinesLoadStatus::SchemaError) {
-            QMessageBox::warning(
-                Core::ICore::dialogParent(),
-                Tr::tr(TrConstants::AGENT_PIPELINES),
-                tr("pipelines.toml has issues — using defaults for affected entries:\n%1\n\n"
-                   "Click OK to continue. Changes you make here will overwrite the file.")
-                    .arg(lr.message));
-        }
-
-        AgentFactory *factory = m_agentFactory.data();
-        m_rosters[0]->setRoster(lr.rosters.codeCompletion, factory);
-        m_rosters[1]->setRoster(lr.rosters.chatAssistant, factory);
-        m_rosters[2]->setRoster(lr.rosters.chatCompression, factory);
-        m_rosters[3]->setRoster(lr.rosters.quickRefactor, factory);
-    }
-
-    QPointer m_agentFactory;
-    QPointer m_navigator;
-    QPointer m_agentsNavigator;
-
-    QLabel *m_titleLabel = nullptr;
-    QPushButton *m_resetBtn = nullptr;
-
-    AgentRosterWidget *m_rosters[kRosterCount] = {};
-
-    QTimer *m_saveDebounce = nullptr;
-    bool m_saveErrorShown = false;
-};
-
-class AgentPipelinesOptionsPage final : public Core::IOptionsPage
-{
-public:
-    AgentPipelinesOptionsPage(
-        AgentFactory *agentFactory,
-        AgentPipelinesPageNavigator *navigator,
-        AgentsPageNavigator *agentsNavigator)
-    {
-        setId(Constants::QODE_ASSIST_AGENT_PIPELINES_PAGE_ID);
-        setDisplayName(Tr::tr(TrConstants::AGENT_PIPELINES));
-        setCategory(Constants::QODE_ASSIST_GENERAL_OPTIONS_CATEGORY);
-        const QPointer factoryPtr(agentFactory);
-        const QPointer navPtr(navigator);
-        const QPointer agentsNavPtr(agentsNavigator);
-        setWidgetCreator([factoryPtr, navPtr, agentsNavPtr] {
-            return new AgentPipelinesPageWidget(factoryPtr, navPtr, agentsNavPtr);
-        });
-    }
-};
-
-} // namespace
-
-std::unique_ptr createAgentPipelinesSettingsPage(
-    AgentFactory *agentFactory,
-    AgentPipelinesPageNavigator *navigator,
-    AgentsPageNavigator *agentsNavigator)
-{
-    return std::make_unique(
-        agentFactory, navigator, agentsNavigator);
-}
-
-} // namespace QodeAssist::Settings
-
-#include "AgentPipelinesPage.moc"
diff --git a/sources/settings/AgentPipelinesPage.hpp b/sources/settings/AgentPipelinesPage.hpp
deleted file mode 100644
index 17b6afe..0000000
--- a/sources/settings/AgentPipelinesPage.hpp
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include 
-#include 
-
-namespace Core {
-class IOptionsPage;
-}
-
-namespace QodeAssist {
-class AgentFactory;
-}
-
-namespace QodeAssist::Settings {
-
-class AgentsPageNavigator;
-
-class AgentPipelinesPageNavigator : public QObject
-{
-    Q_OBJECT
-    Q_DISABLE_COPY_MOVE(AgentPipelinesPageNavigator)
-public:
-    explicit AgentPipelinesPageNavigator(QObject *parent = nullptr);
-
-signals:
-    void editAgentRequested(const QString &agentName);
-};
-
-std::unique_ptr createAgentPipelinesSettingsPage(
-    AgentFactory *agentFactory,
-    AgentPipelinesPageNavigator *navigator,
-    AgentsPageNavigator *agentsNavigator);
-
-} // namespace QodeAssist::Settings
diff --git a/sources/settings/AgentRosterWidget.cpp b/sources/settings/AgentRosterWidget.cpp
deleted file mode 100644
index ebb134e..0000000
--- a/sources/settings/AgentRosterWidget.cpp
+++ /dev/null
@@ -1,637 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "AgentRosterWidget.hpp"
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-
-#include "AgentSelectionDialog.hpp"
-#include "SettingsTr.hpp"
-
-namespace QodeAssist::Settings {
-
-namespace {
-
-enum class PillKind {
-    Template,
-    On,       // capability on (thinking/tools)
-    Off,      // capability off ("plain")
-    User,     // user-defined agent
-    Active,   // ✓ active
-    Match,    // matched-this-row chip background
-    Tag,      // free-form discoverability tag from AgentConfig::tags
-    Neutral,
-};
-
-struct Theme
-{
-    bool dark = false;
-
-    QColor pageBg;
-    QColor cardBg;
-    QColor cardBorder;
-    QColor groupBorder;
-    QColor rowSeparator;
-    QColor rowMatchBg;
-    QColor listHeader;
-
-    QColor text;
-    QColor textSoft;
-    QColor textMute;
-    QColor textFaint;
-
-    QColor matchChipBg;
-    QColor matchChipBorder;
-    QColor matchChipText;
-
-    QColor codeBg;
-    QColor codeBorder;
-
-    struct Pill
-    {
-        QColor bg, fg, border;
-    };
-    Pill pill(PillKind k) const;
-};
-
-Theme::Pill Theme::pill(PillKind k) const
-{
-    if (dark) {
-        switch (k) {
-        case PillKind::Template: return {{0x2c, 0x3f, 0x5a}, {0xcf, 0xe1, 0xf7}, {0x4a, 0x62, 0x86}};
-        case PillKind::On:       return {{0x2f, 0x45, 0x30}, {0xbc, 0xe0, 0xbd}, {0x4a, 0x6c, 0x4b}};
-        case PillKind::Off:      return {{0x3a, 0x3a, 0x3a}, {0x8a, 0x8a, 0x8a}, {0x4a, 0x4a, 0x4a}};
-        case PillKind::User:     return {{0x4a, 0x3f, 0x24}, {0xe6, 0xcd, 0x92}, {0x6a, 0x5a, 0x30}};
-        case PillKind::Active:   return {{0x3a, 0x6a, 0x28}, {0xff, 0xff, 0xff}, {0x4a, 0x80, 0x38}};
-        case PillKind::Match:    return {{0x27, 0x38, 0x4a}, {0xbd, 0xd7, 0xee}, {0x3f, 0x5a, 0x78}};
-        case PillKind::Tag:      return {{0x2e, 0x2e, 0x3a}, {0xb9, 0xb9, 0xcf}, {0x46, 0x46, 0x5a}};
-        case PillKind::Neutral:  return {{0x3a, 0x3a, 0x3a}, {0xcf, 0xcf, 0xcf}, {0x4a, 0x4a, 0x4a}};
-        }
-    } else {
-        switch (k) {
-        case PillKind::Template: return {{0xdb, 0xe7, 0xf6}, {0x1f, 0x3f, 0x73}, {0xa8, 0xc1, 0xe0}};
-        case PillKind::On:       return {{0xdb, 0xe9, 0xd3}, {0x2c, 0x5a, 0x1c}, {0xa3, 0xbc, 0x97}};
-        case PillKind::Off:      return {{0xec, 0xec, 0xec}, {0x7a, 0x7a, 0x7a}, {0xc8, 0xc8, 0xc8}};
-        case PillKind::User:     return {{0xf0, 0xe4, 0xcf}, {0x75, 0x54, 0x1a}, {0xcd, 0xb9, 0x8a}};
-        case PillKind::Active:   return {{0x2c, 0x5a, 0x1c}, {0xff, 0xff, 0xff}, {0x2c, 0x5a, 0x1c}};
-        case PillKind::Match:    return {{0xd6, 0xe8, 0xf7}, {0x1a, 0x4a, 0x7a}, {0x8a, 0xb1, 0xd5}};
-        case PillKind::Tag:      return {{0xe7, 0xe7, 0xf2}, {0x46, 0x46, 0x6e}, {0xc1, 0xc1, 0xd5}};
-        case PillKind::Neutral:  return {{0xe3, 0xe3, 0xe3}, {0x3a, 0x3a, 0x3a}, {0xbc, 0xbc, 0xbc}};
-        }
-    }
-    return {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
-}
-
-Theme themeFor(const QWidget *w)
-{
-    const QPalette pal = w ? w->palette() : QApplication::palette();
-    const bool dark = pal.color(QPalette::Window).lightness() < 128;
-    Theme t;
-    t.dark = dark;
-    if (dark) {
-        t.pageBg = {0x2b, 0x2b, 0x2b};
-        t.cardBg = {0x2f, 0x2f, 0x2f};
-        t.cardBorder = {0x4a, 0x4a, 0x4a};
-        t.groupBorder = {0x4a, 0x4a, 0x4a};
-        t.rowSeparator = {0x3a, 0x3a, 0x3a};
-        t.rowMatchBg = {0x2d, 0x3d, 0x24};
-        t.listHeader = {0x25, 0x25, 0x25};
-        t.text = {0xe6, 0xe6, 0xe6};
-        t.textSoft = {0xc2, 0xc2, 0xc2};
-        t.textMute = {0x9a, 0x9a, 0x9a};
-        t.textFaint = {0x7a, 0x7a, 0x7a};
-        t.matchChipBg = {0x26, 0x26, 0x26};
-        t.matchChipBorder = {0x3a, 0x3a, 0x3a};
-        t.matchChipText = {0xc2, 0xc2, 0xc2};
-        t.codeBg = {0x26, 0x26, 0x26};
-        t.codeBorder = {0x3a, 0x3a, 0x3a};
-    } else {
-        t.pageBg = {0xed, 0xed, 0xed};
-        t.cardBg = {0xf8, 0xf8, 0xf8};
-        t.cardBorder = {0xbd, 0xbd, 0xbd};
-        t.groupBorder = {0xb8, 0xb8, 0xb8};
-        t.rowSeparator = {0xe0, 0xe0, 0xe0};
-        t.rowMatchBg = {0xe8, 0xf3, 0xdf};
-        t.listHeader = {0xe8, 0xe8, 0xe8};
-        t.text = {0x1c, 0x1c, 0x1c};
-        t.textSoft = {0x3a, 0x3a, 0x3a};
-        t.textMute = {0x5a, 0x5a, 0x5a};
-        t.textFaint = {0x8a, 0x8a, 0x8a};
-        t.matchChipBg = {0xea, 0xea, 0xea};
-        t.matchChipBorder = {0xcf, 0xcf, 0xcf};
-        t.matchChipText = {0x3a, 0x3a, 0x3a};
-        t.codeBg = {0xea, 0xea, 0xea};
-        t.codeBorder = {0xcf, 0xcf, 0xcf};
-    }
-    return t;
-}
-
-QString hex(const QColor &c)
-{
-    return c.name(QColor::HexRgb);
-}
-
-QString pillStyle(const Theme &t, PillKind k)
-{
-    const auto p = t.pill(k);
-    return QStringLiteral(
-               "background:%1; color:%2; border:1px solid %3;"
-               "padding:0px 5px; border-radius:2px;"
-               "font-size:10px;")
-        .arg(hex(p.bg), hex(p.fg), hex(p.border));
-}
-
-QLabel *makePill(const QString &text, PillKind kind, const Theme &t, QWidget *parent = nullptr)
-{
-    auto *l = new QLabel(text, parent);
-    l->setStyleSheet(pillStyle(t, kind));
-    l->setAlignment(Qt::AlignCenter);
-    return l;
-}
-
-struct MatchSummary
-{
-    QString icon;
-    QString kind;
-    QString value;
-};
-
-MatchSummary summarise(const AgentConfig::Match &m)
-{
-    if (m.isEmpty())
-        return {QStringLiteral("·"), AgentRosterWidget::tr("fallback"),
-                AgentRosterWidget::tr("any file")};
-    if (!m.filePatterns.isEmpty())
-        return {QStringLiteral("*"), AgentRosterWidget::tr("path"),
-                m.filePatterns.join(QStringLiteral(", "))};
-    if (!m.pathPatterns.isEmpty())
-        return {QStringLiteral("*"), AgentRosterWidget::tr("path"),
-                m.pathPatterns.join(QStringLiteral(", "))};
-    if (!m.projectNames.isEmpty())
-        return {QStringLiteral("▣"), AgentRosterWidget::tr("project"),
-                m.projectNames.join(QStringLiteral(", "))};
-    return {QStringLiteral("·"), AgentRosterWidget::tr("any"), {}};
-}
-
-} // namespace
-
-class AgentRosterRow : public QFrame
-{
-    Q_OBJECT
-public:
-    AgentRosterRow(int index,
-                   const QString &name,
-                   const AgentConfig *cfg,
-                   bool active,
-                   bool first,
-                   bool last,
-                   const Theme &theme,
-                   QWidget *parent = nullptr);
-
-signals:
-    void moveUpRequested(int index);
-    void moveDownRequested(int index);
-    void editRequested(int index);
-    void removeRequested(int index);
-
-private:
-    QWidget *buildIdentityLine(const QString &displayName,
-                               const QString &model,
-                               bool active,
-                               bool isUser,
-                               const Theme &t);
-    QWidget *buildMetaLine(const AgentConfig *cfg, bool active, const Theme &t);
-    QWidget *buildActions(const Theme &t, bool first, bool last);
-
-    int m_index;
-};
-
-AgentRosterRow::AgentRosterRow(int index,
-                               const QString &name,
-                               const AgentConfig *cfg,
-                               bool active,
-                               bool first,
-                               bool last,
-                               const Theme &theme,
-                               QWidget *parent)
-    : QFrame(parent), m_index(index)
-{
-    setAutoFillBackground(true);
-    QPalette pal = palette();
-    pal.setColor(QPalette::Window, active ? theme.rowMatchBg : theme.cardBg);
-    setPalette(pal);
-
-    setStyleSheet(QStringLiteral("AgentRosterRow { border:0; border-top:%1; }")
-                      .arg(index == 0 ? QStringLiteral("0px")
-                                      : QStringLiteral("1px solid %1").arg(hex(theme.rowSeparator))));
-
-    auto *outer = new QHBoxLayout(this);
-    outer->setContentsMargins(8, 6, 8, 6);
-    outer->setSpacing(8);
-
-    auto *moveCol = new QVBoxLayout;
-    moveCol->setContentsMargins(0, 0, 0, 0);
-    moveCol->setSpacing(1);
-    auto makeArrow = [&](const QString &glyph, const QString &tip, bool enabled) {
-        auto *b = new QToolButton(this);
-        b->setText(glyph);
-        b->setToolTip(tip);
-        b->setEnabled(enabled);
-        b->setAutoRaise(true);
-        b->setFixedSize(18, 14);
-        QFont f = b->font();
-        f.setPointSizeF(f.pointSizeF() * 0.75);
-        b->setFont(f);
-        return b;
-    };
-    auto *upBtn = makeArrow(QStringLiteral("▲"), tr("Move up"), !first);
-    auto *dnBtn = makeArrow(QStringLiteral("▼"), tr("Move down"), !last);
-    moveCol->addWidget(upBtn);
-    moveCol->addWidget(dnBtn);
-    outer->addLayout(moveCol);
-
-    auto *idxLbl = new QLabel(QStringLiteral("%1.").arg(index + 1), this);
-    QFont monoFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
-    idxLbl->setFont(monoFont);
-    QPalette idxPal = idxLbl->palette();
-    idxPal.setColor(QPalette::WindowText, active ? theme.text : theme.textFaint);
-    idxLbl->setPalette(idxPal);
-    idxLbl->setFixedWidth(22);
-    idxLbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
-    outer->addWidget(idxLbl);
-
-    auto *body = new QVBoxLayout;
-    body->setContentsMargins(0, 0, 0, 0);
-    body->setSpacing(2);
-
-    const QString displayName = cfg ? cfg->name : tr("%1 (missing)").arg(name);
-    const QString model = cfg ? cfg->model : QString();
-    const bool isUser = cfg && cfg->isUserSource();
-
-    body->addWidget(buildIdentityLine(displayName, model, active, isUser, theme));
-    body->addWidget(buildMetaLine(cfg, active, theme));
-    outer->addLayout(body, /*stretch*/ 1);
-
-    outer->addWidget(buildActions(theme, first, last));
-
-    if (cfg && !cfg->description.isEmpty())
-        setToolTip(cfg->description);
-
-    connect(upBtn, &QToolButton::clicked, this, [this]() { emit moveUpRequested(m_index); });
-    connect(dnBtn, &QToolButton::clicked, this, [this]() { emit moveDownRequested(m_index); });
-}
-
-QWidget *AgentRosterRow::buildIdentityLine(const QString &displayName,
-                                           const QString &model,
-                                           bool active,
-                                           bool isUser,
-                                           const Theme &t)
-{
-    auto *w = new QWidget(this);
-    auto *line = new QHBoxLayout(w);
-    line->setContentsMargins(0, 0, 0, 0);
-    line->setSpacing(6);
-
-    auto *nameLbl = new QLabel(displayName, w);
-    QFont nameFont = nameLbl->font();
-    nameFont.setBold(true);
-    nameLbl->setFont(nameFont);
-    line->addWidget(nameLbl);
-
-    if (!model.isEmpty()) {
-        auto *modelLbl = new QLabel(model, w);
-        modelLbl->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
-        QPalette mp = modelLbl->palette();
-        mp.setColor(QPalette::WindowText, t.textSoft);
-        modelLbl->setPalette(mp);
-        line->addWidget(modelLbl);
-    }
-
-    if (active)
-        line->addWidget(makePill(QStringLiteral("✓ ") + tr("active"), PillKind::Active, t, w));
-    if (isUser)
-        line->addWidget(makePill(tr("user"), PillKind::User, t, w));
-
-    line->addStretch(1);
-    return w;
-}
-
-QWidget *AgentRosterRow::buildMetaLine(const AgentConfig *cfg, bool active, const Theme &t)
-{
-    auto *w = new QWidget(this);
-    auto *line = new QHBoxLayout(w);
-    line->setContentsMargins(0, 0, 0, 0);
-    line->setSpacing(4);
-
-    if (cfg) {
-        const auto sm = summarise(cfg->match);
-        auto *chip = new QLabel(w);
-        const QColor bg = active ? t.pill(PillKind::Match).bg : t.matchChipBg;
-        const QColor bd = active ? t.pill(PillKind::Match).border : t.matchChipBorder;
-        const QColor fg = active ? t.pill(PillKind::Match).fg : t.matchChipText;
-        QString chipText = QStringLiteral(
-                               "%1 "
-                               "%3: %4")
-                               .arg(sm.icon,
-                                    hex(active ? t.pill(PillKind::Match).fg : t.textFaint),
-                                    sm.kind,
-                                    sm.value.toHtmlEscaped());
-        chip->setTextFormat(Qt::RichText);
-        chip->setText(chipText);
-        chip->setStyleSheet(QStringLiteral("background:%1; color:%2; border:1px solid %3;"
-                                           "padding:0px 6px; border-radius:2px; font-size:11px;")
-                                .arg(hex(bg), hex(fg), hex(bd)));
-        chip->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
-        line->addWidget(chip);
-
-        auto *arrow = new QLabel(QStringLiteral("→"), w);
-        QPalette ap = arrow->palette();
-        ap.setColor(QPalette::WindowText, t.textFaint);
-        arrow->setPalette(ap);
-        line->addWidget(arrow);
-
-        if (!cfg->providerInstance.isEmpty())
-            line->addWidget(makePill(cfg->providerInstance, PillKind::Template, t, w));
-
-        constexpr int kMaxTagPills = 3;
-        const int tagCount = cfg->tags.size();
-        for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i)
-            line->addWidget(makePill(cfg->tags.at(i), PillKind::Tag, t, w));
-        if (tagCount > kMaxTagPills) {
-            auto *more = makePill(QStringLiteral("+%1").arg(tagCount - kMaxTagPills),
-                                  PillKind::Tag, t, w);
-            more->setToolTip(cfg->tags.mid(kMaxTagPills).join(QStringLiteral(", ")));
-            line->addWidget(more);
-        }
-    } else {
-        auto *missing = new QLabel(tr("agent configuration is no longer available"), w);
-        QPalette mp = missing->palette();
-        mp.setColor(QPalette::WindowText, t.textMute);
-        missing->setPalette(mp);
-        line->addWidget(missing);
-    }
-    line->addStretch(1);
-    return w;
-}
-
-QWidget *AgentRosterRow::buildActions(const Theme &, bool /*first*/, bool /*last*/)
-{
-    auto *w = new QWidget(this);
-    auto *l = new QHBoxLayout(w);
-    l->setContentsMargins(0, 0, 0, 0);
-    l->setSpacing(4);
-
-    auto *edit = new QToolButton(w);
-    edit->setText(tr("Edit…"));
-    edit->setToolButtonStyle(Qt::ToolButtonTextOnly);
-    edit->setAutoRaise(false);
-    edit->setFocusPolicy(Qt::TabFocus);
-    edit->setMinimumHeight(22);
-
-    auto *remove = new QToolButton(w);
-    remove->setText(QStringLiteral("✕"));
-    remove->setToolTip(tr("Remove from list"));
-    remove->setAutoRaise(false);
-    remove->setFixedSize(22, 22);
-
-    l->addWidget(edit);
-    l->addWidget(remove);
-
-    connect(edit, &QToolButton::clicked, this, [this]() { emit editRequested(m_index); });
-    connect(remove, &QToolButton::clicked, this, [this]() { emit removeRequested(m_index); });
-
-    return w;
-}
-
-AgentRosterWidget::AgentRosterWidget(QWidget *parent)
-    : QWidget(parent)
-{
-    const Theme t = themeFor(this);
-
-    auto *outer = new QVBoxLayout(this);
-    outer->setContentsMargins(0, 0, 0, 0);
-    outer->setSpacing(6);
-
-    auto *titleRow = new QHBoxLayout;
-    titleRow->setContentsMargins(0, 0, 0, 0);
-    titleRow->setSpacing(6);
-    m_accentDot = new QLabel(this);
-    m_accentDot->setFixedSize(10, 10);
-    m_titleLabel = new QLabel(this);
-    QFont tf = m_titleLabel->font();
-    tf.setBold(true);
-    m_titleLabel->setFont(tf);
-    titleRow->addWidget(m_accentDot);
-    titleRow->addWidget(m_titleLabel);
-    titleRow->addStretch(1);
-    outer->addLayout(titleRow);
-
-    m_hintLabel = new QLabel(this);
-    m_hintLabel->setWordWrap(true);
-    QPalette hp = m_hintLabel->palette();
-    hp.setColor(QPalette::WindowText, t.textMute);
-    m_hintLabel->setPalette(hp);
-    QFont hf = m_hintLabel->font();
-    hf.setPointSizeF(hf.pointSizeF() * 0.9);
-    m_hintLabel->setFont(hf);
-    outer->addWidget(m_hintLabel);
-
-    m_rowsFrame = new QFrame(this);
-    m_rowsFrame->setObjectName(QStringLiteral("rosterCard"));
-    m_rowsFrame->setStyleSheet(
-        QStringLiteral("QFrame#rosterCard { background:%1; border:1px solid %2; border-radius:2px; }")
-            .arg(hex(t.cardBg), hex(t.cardBorder)));
-    m_rowsLayout = new QVBoxLayout(m_rowsFrame);
-    m_rowsLayout->setContentsMargins(0, 0, 0, 0);
-    m_rowsLayout->setSpacing(0);
-
-    m_emptyHint = new QLabel(tr("No agents in roster. Click \"Add agent…\" to populate."),
-                             m_rowsFrame);
-    m_emptyHint->setAlignment(Qt::AlignCenter);
-    m_emptyHint->setContentsMargins(10, 12, 10, 12);
-    QFont eh = m_emptyHint->font();
-    eh.setItalic(true);
-    m_emptyHint->setFont(eh);
-    QPalette ep = m_emptyHint->palette();
-    ep.setColor(QPalette::WindowText, t.textFaint);
-    m_emptyHint->setPalette(ep);
-    m_rowsLayout->addWidget(m_emptyHint);
-
-    outer->addWidget(m_rowsFrame);
-
-    auto *footer = new QHBoxLayout;
-    footer->setContentsMargins(0, 0, 0, 0);
-    footer->setSpacing(6);
-    m_addBtn = new QPushButton(tr("+ Add agent…"), this);
-    footer->addWidget(m_addBtn);
-    footer->addStretch(1);
-    m_footerHint = new QLabel(tr("first matching agent is used"), this);
-    QPalette fp = m_footerHint->palette();
-    fp.setColor(QPalette::WindowText, t.textMute);
-    m_footerHint->setPalette(fp);
-    QFont ff = m_footerHint->font();
-    ff.setPointSizeF(ff.pointSizeF() * 0.9);
-    m_footerHint->setFont(ff);
-    footer->addWidget(m_footerHint);
-    outer->addLayout(footer);
-
-    connect(m_addBtn, &QPushButton::clicked, this, &AgentRosterWidget::onAddClicked);
-}
-
-void AgentRosterWidget::setSlot(const QString &title, const QString &hint, const QColor &accent)
-{
-    m_titleLabel->setText(title);
-    m_hintLabel->setText(hint);
-    m_hintLabel->setVisible(!hint.isEmpty());
-    if (accent.isValid()) {
-        m_accentDot->setStyleSheet(
-            QStringLiteral("background:%1; border:1px solid %2;")
-                .arg(accent.name(), accent.darker(140).name()));
-        m_accentDot->setVisible(true);
-    } else {
-        m_accentDot->setStyleSheet({});
-        m_accentDot->setVisible(false);
-    }
-}
-
-void AgentRosterWidget::setRoster(const QStringList &names, AgentFactory *factory)
-{
-    m_factory = factory;
-    m_names = names;
-    rebuildRows();
-}
-
-void AgentRosterWidget::setRoutingContext(const AgentRouter::Context &ctx)
-{
-    m_routingCtx = ctx;
-    recomputeActive();
-    rebuildRows();
-}
-
-void AgentRosterWidget::recomputeActive()
-{
-    if (!m_factory || m_names.isEmpty()
-        || (m_routingCtx.filePath.isEmpty() && m_routingCtx.projectName.isEmpty())) {
-        m_activeIndex = -1;
-        return;
-    }
-    const QString picked = AgentRouter::pickAgent(m_names, m_routingCtx, *m_factory);
-    m_activeIndex = picked.isEmpty() ? -1 : m_names.indexOf(picked);
-}
-
-void AgentRosterWidget::rebuildRows()
-{
-    // Tear down existing row widgets (keep the empty hint).
-    while (m_rowsLayout->count() > 0) {
-        QLayoutItem *it = m_rowsLayout->takeAt(0);
-        if (auto *w = it->widget()) {
-            if (w == m_emptyHint)
-                w->setVisible(false);
-            else
-                w->deleteLater();
-        }
-        delete it;
-    }
-
-    if (m_names.isEmpty()) {
-        m_emptyHint->setVisible(true);
-        m_rowsLayout->addWidget(m_emptyHint);
-        return;
-    }
-
-    recomputeActive();
-    const Theme t = themeFor(this);
-
-    for (int i = 0; i < m_names.size(); ++i) {
-        const QString &name = m_names.at(i);
-        const AgentConfig *cfg = m_factory ? m_factory->configByName(name) : nullptr;
-        auto *row = new AgentRosterRow(i,
-                                       name,
-                                       cfg,
-                                       i == m_activeIndex,
-                                       /*first*/ i == 0,
-                                       /*last*/ i == m_names.size() - 1,
-                                       t,
-                                       m_rowsFrame);
-        connect(row, &AgentRosterRow::moveUpRequested, this, &AgentRosterWidget::onRowMoveUp);
-        connect(row, &AgentRosterRow::moveDownRequested, this, &AgentRosterWidget::onRowMoveDown);
-        connect(row, &AgentRosterRow::editRequested, this, &AgentRosterWidget::onRowEdit);
-        connect(row, &AgentRosterRow::removeRequested, this, &AgentRosterWidget::onRowRemove);
-        m_rowsLayout->addWidget(row);
-    }
-}
-
-void AgentRosterWidget::onAddClicked()
-{
-    if (!m_factory)
-        return;
-
-    AgentSelectionDialog dialog(m_factory->configs(),
-                                /*currentName*/ QString{},
-                                m_factory.data(),
-                                Core::ICore::dialogParent());
-    if (dialog.exec() != QDialog::Accepted)
-        return;
-    const QString picked = dialog.selectedName();
-    if (picked.isEmpty() || m_names.contains(picked))
-        return;
-
-    m_names.append(picked);
-    rebuildRows();
-    emit rosterChanged(m_names);
-}
-
-void AgentRosterWidget::onRowMoveUp(int index)
-{
-    if (index <= 0 || index >= m_names.size())
-        return;
-    m_names.swapItemsAt(index, index - 1);
-    rebuildRows();
-    emit rosterChanged(m_names);
-}
-
-void AgentRosterWidget::onRowMoveDown(int index)
-{
-    if (index < 0 || index >= m_names.size() - 1)
-        return;
-    m_names.swapItemsAt(index, index + 1);
-    rebuildRows();
-    emit rosterChanged(m_names);
-}
-
-void AgentRosterWidget::onRowRemove(int index)
-{
-    if (index < 0 || index >= m_names.size())
-        return;
-    m_names.removeAt(index);
-    rebuildRows();
-    emit rosterChanged(m_names);
-}
-
-void AgentRosterWidget::onRowEdit(int index)
-{
-    if (index < 0 || index >= m_names.size())
-        return;
-    emit editAgentRequested(m_names.at(index));
-}
-
-} // namespace QodeAssist::Settings
-
-#include "AgentRosterWidget.moc"
diff --git a/sources/settings/AgentRosterWidget.hpp b/sources/settings/AgentRosterWidget.hpp
deleted file mode 100644
index 4a78db5..0000000
--- a/sources/settings/AgentRosterWidget.hpp
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-class QLabel;
-class QToolButton;
-class QPushButton;
-class QVBoxLayout;
-class QFrame;
-
-namespace QodeAssist {
-class AgentFactory;
-}
-
-namespace QodeAssist::Settings {
-
-class AgentRosterRow;
-
-class AgentRosterWidget : public QWidget
-{
-    Q_OBJECT
-public:
-    explicit AgentRosterWidget(QWidget *parent = nullptr);
-
-    void setSlot(const QString &title, const QString &hint, const QColor &accent);
-    void setRoster(const QStringList &names, AgentFactory *factory);
-
-    [[nodiscard]] QStringList roster() const { return m_names; }
-
-    void setRoutingContext(const AgentRouter::Context &ctx);
-
-signals:
-    void rosterChanged(const QStringList &names);
-    void editAgentRequested(const QString &agentName);
-
-private:
-    void rebuildRows();
-    void recomputeActive();
-
-    void onAddClicked();
-    void onRowMoveUp(int index);
-    void onRowMoveDown(int index);
-    void onRowRemove(int index);
-    void onRowEdit(int index);
-
-    QStringList m_names;
-    QPointer m_factory;
-    AgentRouter::Context m_routingCtx;
-    int m_activeIndex = -1;
-
-    QLabel *m_accentDot = nullptr;
-    QLabel *m_titleLabel = nullptr;
-    QLabel *m_hintLabel = nullptr;
-    QFrame *m_rowsFrame = nullptr;
-    QVBoxLayout *m_rowsLayout = nullptr;
-    QLabel *m_emptyHint = nullptr;
-    QPushButton *m_addBtn = nullptr;
-    QLabel *m_footerHint = nullptr;
-};
-
-} // namespace QodeAssist::Settings
diff --git a/sources/settings/AgentSelectionDialog.cpp b/sources/settings/AgentSelectionDialog.cpp
deleted file mode 100644
index 7b98069..0000000
--- a/sources/settings/AgentSelectionDialog.cpp
+++ /dev/null
@@ -1,472 +0,0 @@
-// Copyright (C) 2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "AgentSelectionDialog.hpp"
-
-#include "AgentSlotWidget.hpp"
-#include "PipelinesConfig.hpp"
-#include "SettingsTr.hpp"
-
-#include 
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace QodeAssist::Settings {
-
-// -- ListRowCard -------------------------------------------------------
-
-ListRowCard::ListRowCard(QWidget *parent)
-    : QFrame(parent)
-{
-    setObjectName(QStringLiteral("ListRowCard"));
-    setAttribute(Qt::WA_StyledBackground, true);
-    setCursor(Qt::PointingHandCursor);
-    setFrameShape(QFrame::NoFrame);
-    applyTheme();
-}
-
-bool ListRowCard::matches(const QString &needle) const
-{
-    if (needle.isEmpty())
-        return true;
-    return m_searchHaystack.contains(needle.toLower());
-}
-
-void ListRowCard::setSelected(bool selected)
-{
-    if (m_selected == selected)
-        return;
-    m_selected = selected;
-    applyTheme();
-}
-
-void ListRowCard::buildSearchHaystack(const QStringList &parts)
-{
-    m_searchHaystack = parts.join(QLatin1Char(' ')).toLower();
-}
-
-void ListRowCard::mousePressEvent(QMouseEvent *event)
-{
-    if (event->button() == Qt::LeftButton)
-        emit clicked();
-    QFrame::mousePressEvent(event);
-}
-
-void ListRowCard::mouseDoubleClickEvent(QMouseEvent *event)
-{
-    if (event->button() == Qt::LeftButton)
-        emit activated();
-    QFrame::mouseDoubleClickEvent(event);
-}
-
-void ListRowCard::enterEvent(QEnterEvent *event)
-{
-    QFrame::enterEvent(event);
-    m_hover = true;
-    applyTheme();
-}
-
-void ListRowCard::leaveEvent(QEvent *event)
-{
-    QFrame::leaveEvent(event);
-    m_hover = false;
-    applyTheme();
-}
-
-void ListRowCard::changeEvent(QEvent *event)
-{
-    QFrame::changeEvent(event);
-    if (m_inApplyTheme)
-        return;
-    if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
-        applyTheme();
-}
-
-void ListRowCard::applyTheme()
-{
-    if (m_inApplyTheme)
-        return;
-    QScopedValueRollback guard(m_inApplyTheme, true);
-
-    const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
-    QString bg = t.bg;
-    QString bd = t.cardBd;
-    if (m_selected) {
-        bg = t.selectedBg;
-        bd = t.selectedBd;
-    } else if (m_hover) {
-        bg = t.hoverBg;
-    }
-    setStyleSheet(QStringLiteral(
-                      "#ListRowCard { background-color: %1; border: 1px solid %2; }")
-                      .arg(bg, bd));
-}
-
-// -- AgentRowCard ------------------------------------------------------
-
-AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
-    : ListRowCard(parent)
-{
-    setItemName(cfg.name);
-    QStringList haystack{cfg.name, cfg.providerInstance, cfg.model,
-                         cfg.description, cfg.role,
-                         cfg.endpoint};
-    haystack += cfg.tags;
-    buildSearchHaystack(haystack);
-
-    auto *name = new QLabel(cfg.name, this);
-    QFont nameFont = name->font();
-    nameFont.setBold(true);
-    nameFont.setPixelSize(13);
-    name->setFont(nameFont);
-    name->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
-
-    QLabel *model = nullptr;
-    if (!cfg.model.isEmpty()) {
-        model = new QLabel(QStringLiteral("· %1").arg(cfg.model), this);
-        model->setFont(CardStyle::monoFont(11));
-        model->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
-        model->setMinimumWidth(0);
-    }
-
-    Pill *sourcePill = nullptr;
-    if (cfg.isUserSource()) {
-        sourcePill = new Pill(
-            Pill::User,
-            cfg.overridesBundled ? Tr::tr("Override") : Tr::tr("User"),
-            this);
-    }
-
-    auto *description = new QLabel(this);
-    description->setWordWrap(false);
-    QFont descFont = description->font();
-    descFont.setItalic(true);
-    description->setFont(descFont);
-    description->setText(cfg.description.isEmpty()
-                             ? Tr::tr("No description provided.")
-                             : cfg.description);
-    description->setMinimumWidth(0);
-    description->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
-    description->setTextInteractionFlags(Qt::NoTextInteraction);
-
-    QStringList endpointParts;
-    if (!cfg.endpoint.isEmpty())
-        endpointParts << cfg.endpoint;
-    endpointParts << (cfg.enableThinking ? Tr::tr("thinking") : Tr::tr("no-thinking"));
-    endpointParts << (cfg.enableTools ? Tr::tr("tools") : Tr::tr("no-tools"));
-
-    auto *endpoint = new QLabel(endpointParts.join(QStringLiteral(" · ")), this);
-    endpoint->setFont(CardStyle::monoFont(11));
-    endpoint->setMinimumWidth(0);
-    endpoint->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
-    endpoint->setTextInteractionFlags(Qt::NoTextInteraction);
-
-    auto *headerRow = new QHBoxLayout;
-    headerRow->setContentsMargins(0, 0, 0, 0);
-    headerRow->setSpacing(6);
-    headerRow->addWidget(name);
-    if (model)
-        headerRow->addWidget(model, 1);
-    else
-        headerRow->addStretch(1);
-    if (sourcePill)
-        headerRow->addWidget(sourcePill);
-
-    auto *outer = new QVBoxLayout(this);
-    outer->setContentsMargins(10, 8, 10, 8);
-    outer->setSpacing(3);
-    outer->addLayout(headerRow);
-    outer->addWidget(description);
-    outer->addWidget(endpoint);
-
-    if (!cfg.tags.isEmpty()) {
-        constexpr int kMaxTagPills = 4;
-        auto *tagsRow = new QHBoxLayout;
-        tagsRow->setContentsMargins(0, 0, 0, 0);
-        tagsRow->setSpacing(4);
-        const int tagCount = cfg.tags.size();
-        for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i)
-            tagsRow->addWidget(new Pill(Pill::Tag, cfg.tags.at(i), this));
-        if (tagCount > kMaxTagPills) {
-            auto *more = new Pill(
-                Pill::Tag,
-                QStringLiteral("+%1").arg(tagCount - kMaxTagPills),
-                this);
-            more->setToolTip(cfg.tags.mid(kMaxTagPills).join(QStringLiteral(", ")));
-            tagsRow->addWidget(more);
-        }
-        tagsRow->addStretch(1);
-        outer->addLayout(tagsRow);
-    }
-
-    const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
-    QPalette descPal = description->palette();
-    descPal.setColor(QPalette::WindowText,
-                     QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft));
-    description->setPalette(descPal);
-    QPalette endPal = endpoint->palette();
-    endPal.setColor(QPalette::WindowText, QColor(t.textFaint));
-    endpoint->setPalette(endPal);
-    if (model) {
-        QPalette mp = model->palette();
-        mp.setColor(QPalette::WindowText, QColor(t.textMute));
-        model->setPalette(mp);
-    }
-
-    QString tooltip;
-    if (!cfg.description.isEmpty())
-        tooltip += cfg.description + QStringLiteral("\n\n");
-    if (!cfg.providerInstance.isEmpty())
-        tooltip += Tr::tr("Provider instance: %1\n").arg(cfg.providerInstance);
-    if (!cfg.role.isEmpty())
-        tooltip += Tr::tr("Role: %1\n").arg(cfg.role);
-    if (!cfg.endpoint.isEmpty())
-        tooltip += Tr::tr("Endpoint: %1\n").arg(cfg.endpoint);
-    setToolTip(tooltip.trimmed());
-}
-
-// -- ProviderSection ---------------------------------------------------
-
-ProviderSection::ProviderSection(const QString &name, QWidget *parent)
-    : QWidget(parent)
-{
-    m_arrow = new QLabel(QStringLiteral("▾"));
-    m_label = new QLabel(name);
-    CardStyle::applySectionFont(m_label);
-
-    QFont arrowFont = m_label->font();
-    arrowFont.setCapitalization(QFont::MixedCase);
-    m_arrow->setFont(arrowFont);
-    QPalette ap = m_arrow->palette();
-    ap.setColor(QPalette::WindowText, ap.color(QPalette::Mid));
-    m_arrow->setPalette(ap);
-
-    m_header = new QFrame;
-    m_header->setObjectName(QStringLiteral("ProviderHeader"));
-    m_header->setCursor(Qt::PointingHandCursor);
-    m_header->setFrameShape(QFrame::NoFrame);
-    auto *headerLayout = new QHBoxLayout(m_header);
-    headerLayout->setContentsMargins(2, 4, 2, 2);
-    headerLayout->setSpacing(6);
-    headerLayout->addWidget(m_arrow);
-    headerLayout->addWidget(m_label);
-    headerLayout->addStretch(1);
-    m_header->installEventFilter(this);
-
-    m_content = new QWidget;
-    m_contentLayout = new QVBoxLayout(m_content);
-    m_contentLayout->setContentsMargins(0, 0, 0, 0);
-    m_contentLayout->setSpacing(4);
-    m_content->setVisible(false);
-    m_arrow->setText(QStringLiteral("▸"));
-    m_expanded = false;
-
-    auto *outer = new QVBoxLayout(this);
-    outer->setContentsMargins(0, 0, 0, 0);
-    outer->setSpacing(0);
-    outer->addWidget(m_header);
-    outer->addWidget(m_content);
-}
-
-void ProviderSection::addCard(ListRowCard *card)
-{
-    m_contentLayout->addWidget(card);
-    m_cards.append(card);
-}
-
-int ProviderSection::applyFilter(const QString &needle)
-{
-    int visible = 0;
-    for (auto *card : m_cards) {
-        const bool show = card->matches(needle);
-        card->setVisible(show);
-        if (show)
-            ++visible;
-    }
-    return visible;
-}
-
-void ProviderSection::setExpanded(bool expanded)
-{
-    if (m_expanded == expanded)
-        return;
-    m_expanded = expanded;
-    m_content->setVisible(expanded);
-    m_arrow->setText(expanded ? QStringLiteral("▾") : QStringLiteral("▸"));
-}
-
-bool ProviderSection::eventFilter(QObject *watched, QEvent *event)
-{
-    if (watched == m_header && event->type() == QEvent::MouseButtonRelease) {
-        auto *me = static_cast(event);
-        if (me->button() == Qt::LeftButton) {
-            setExpanded(!m_expanded);
-            return true;
-        }
-    }
-    return QWidget::eventFilter(watched, event);
-}
-
-// -- AgentSelectionDialog ----------------------------------------------
-
-AgentSelectionDialog::AgentSelectionDialog(
-    const std::vector &configs,
-    const QString ¤tName,
-    AgentFactory *agentFactory,
-    QWidget *parent)
-    : QDialog(parent)
-    , m_agentFactory(agentFactory)
-{
-    setWindowTitle(Tr::tr("Change Agent"));
-    resize(720, 600);
-    setSizeGripEnabled(true);
-
-    if (!m_agentFactory)
-        m_localConfigs = configs;
-
-    m_filter = new QLineEdit(this);
-    m_filter->setPlaceholderText(
-        Tr::tr("Filter by name, provider, model, template, description…"));
-    m_filter->setClearButtonEnabled(true);
-
-    auto *topRow = new QHBoxLayout;
-    topRow->setContentsMargins(0, 0, 0, 0);
-    topRow->setSpacing(6);
-    topRow->addWidget(m_filter, 1);
-
-    m_scroll = new QScrollArea(this);
-    m_scroll->setWidgetResizable(true);
-    m_scroll->setFrameShape(QFrame::StyledPanel);
-    m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
-
-    auto *buttons
-        = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
-    m_okButton = buttons->button(QDialogButtonBox::Ok);
-    m_okButton->setText(Tr::tr("Change"));
-    m_okButton->setEnabled(false);
-
-    auto *layout = new QVBoxLayout(this);
-    layout->addLayout(topRow);
-    layout->addWidget(m_scroll);
-    layout->addWidget(buttons);
-
-    rebuild(currentName);
-
-    connect(m_filter, &QLineEdit::textChanged, this,
-            [this](const QString &text) { applyFilter(text); });
-    connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
-    connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
-}
-
-void AgentSelectionDialog::selectCard(ListRowCard *card)
-{
-    if (m_currentCard == card)
-        return;
-    if (m_currentCard)
-        m_currentCard->setSelected(false);
-    m_currentCard = card;
-    if (m_currentCard) {
-        m_currentCard->setSelected(true);
-        m_selectedName = m_currentCard->itemName();
-    } else {
-        m_selectedName.clear();
-    }
-    if (m_okButton)
-        m_okButton->setEnabled(!m_selectedName.isEmpty());
-}
-
-void AgentSelectionDialog::rebuild(const QString ¤tName)
-{
-    m_sections.clear();
-    m_currentCard = nullptr;
-    m_selectedName.clear();
-    if (m_okButton)
-        m_okButton->setEnabled(false);
-
-    const auto &configs
-        = m_agentFactory ? m_agentFactory->configs() : m_localConfigs;
-
-    auto *content = new QWidget;
-    auto *contentLayout = new QVBoxLayout(content);
-    contentLayout->setContentsMargins(12, 12, 12, 12);
-    contentLayout->setSpacing(6);
-
-    QMap> byProvider;
-    for (const auto &cfg : configs) {
-        if (cfg.hidden) continue; // hidden profiles stay loaded but don't surface in the picker
-        const QString key = cfg.providerInstance.isEmpty()
-                                ? Tr::tr("(Unknown provider instance)")
-                                : cfg.providerInstance;
-        byProvider[key].push_back(&cfg);
-    }
-
-    AgentRowCard *toSelect = nullptr;
-    ProviderSection *sectionToExpand = nullptr;
-
-    for (auto it = byProvider.cbegin(); it != byProvider.cend(); ++it) {
-        auto *section = new ProviderSection(it.key());
-        auto sortedConfigs = it.value();
-        std::sort(sortedConfigs.begin(), sortedConfigs.end(),
-                  [](const AgentConfig *a, const AgentConfig *b) { return a->name < b->name; });
-        for (const AgentConfig *cfg : sortedConfigs) {
-            auto *card = new AgentRowCard(*cfg);
-            connect(card, &ListRowCard::clicked, this,
-                    [this, card]() { selectCard(card); });
-            connect(card, &ListRowCard::activated, this, [this, card]() {
-                selectCard(card);
-                accept();
-            });
-            section->addCard(card);
-            if (cfg->name == currentName) {
-                toSelect = card;
-                sectionToExpand = section;
-            }
-        }
-        contentLayout->addWidget(section);
-        m_sections.append(section);
-    }
-    contentLayout->addStretch(1);
-
-    m_scroll->setWidget(content);
-
-    if (sectionToExpand)
-        sectionToExpand->setExpanded(true);
-
-    if (toSelect) {
-        selectCard(toSelect);
-        QTimer::singleShot(0, this, [this, toSelect]() {
-            m_scroll->ensureWidgetVisible(toSelect, 0, 60);
-        });
-    }
-
-    applyFilter(m_filter ? m_filter->text() : QString());
-}
-
-void AgentSelectionDialog::applyFilter(const QString &needle)
-{
-    const QString trimmed = needle.trimmed();
-    for (auto *section : m_sections) {
-        const int visible = section->applyFilter(trimmed);
-        section->setVisible(visible > 0);
-        if (!trimmed.isEmpty())
-            section->setExpanded(visible > 0);
-    }
-}
-
-} // namespace QodeAssist::Settings
diff --git a/sources/settings/AgentSelectionDialog.hpp b/sources/settings/AgentSelectionDialog.hpp
deleted file mode 100644
index 2f879a6..0000000
--- a/sources/settings/AgentSelectionDialog.hpp
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright (C) 2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-class QLineEdit;
-class QPushButton;
-class QScrollArea;
-class QVBoxLayout;
-
-namespace QodeAssist {
-class AgentFactory;
-}
-
-namespace QodeAssist::Settings {
-
-namespace CardStyle {
-
-struct Tone
-{
-    QString bg;
-    QString hoverBg;
-    QString selectedBg;
-    QString selectedBd;
-    QString cardBd;
-    QString textSoft;
-    QString textMute;
-    QString textFaint;
-};
-
-inline bool isDark(const QPalette &p)
-{
-    return p.color(QPalette::Window).lightness() < 128;
-}
-
-inline Tone toneFor(bool dark)
-{
-    return dark
-        ? Tone{"#333333", "#3a3a3a", "#2a4566", "#3a6fb7", "#4a4a4a",
-               "#c2c2c2", "#9a9a9a", "#7a7a7a"}
-        : Tone{"#f6f6f6", "#ececec", "#cfe1f7", "#3a6fb7", "#bdbdbd",
-               "#3a3a3a", "#5a5a5a", "#8a8a8a"};
-}
-
-inline QFont monoFont(int pixelSize)
-{
-    QFont f;
-    f.setFamilies({QStringLiteral("SF Mono"),
-                   QStringLiteral("Cascadia Code"),
-                   QStringLiteral("Consolas"),
-                   QStringLiteral("Liberation Mono"),
-                   QStringLiteral("Menlo"),
-                   QStringLiteral("Courier New")});
-    f.setStyleHint(QFont::Monospace);
-    f.setPixelSize(pixelSize);
-    return f;
-}
-
-inline void applySectionFont(QLabel *l)
-{
-    QFont f = l->font();
-    f.setPointSizeF(f.pointSizeF() * 0.85);
-    f.setBold(true);
-    f.setCapitalization(QFont::AllUppercase);
-    f.setLetterSpacing(QFont::AbsoluteSpacing, 0.5);
-    l->setFont(f);
-    QPalette p = l->palette();
-    p.setColor(QPalette::WindowText, p.color(QPalette::Mid));
-    l->setPalette(p);
-}
-
-} // namespace CardStyle
-
-class ListRowCard : public QFrame
-{
-    Q_OBJECT
-public:
-    QString itemName() const { return m_itemName; }
-    bool matches(const QString &needle) const;
-    void setSelected(bool selected);
-    bool isSelected() const { return m_selected; }
-
-signals:
-    void clicked();
-    void activated();
-
-protected:
-    explicit ListRowCard(QWidget *parent = nullptr);
-
-    void setItemName(const QString &name) { m_itemName = name; }
-    void buildSearchHaystack(const QStringList &parts);
-
-    void mousePressEvent(QMouseEvent *event) override;
-    void mouseDoubleClickEvent(QMouseEvent *event) override;
-    void enterEvent(QEnterEvent *event) override;
-    void leaveEvent(QEvent *event) override;
-    void changeEvent(QEvent *event) override;
-
-private:
-    void applyTheme();
-
-    QString m_itemName;
-    QString m_searchHaystack;
-    bool m_selected = false;
-    bool m_hover = false;
-    bool m_inApplyTheme = false;
-};
-
-class AgentRowCard : public ListRowCard
-{
-    Q_OBJECT
-public:
-    explicit AgentRowCard(const AgentConfig &cfg, QWidget *parent = nullptr);
-
-    QString agentName() const { return itemName(); }
-};
-
-class ProviderSection : public QWidget
-{
-    Q_OBJECT
-public:
-    explicit ProviderSection(const QString &name, QWidget *parent = nullptr);
-
-    void addCard(ListRowCard *card);
-    void setExpanded(bool expanded);
-    bool isExpanded() const { return m_expanded; }
-    const QList &cards() const { return m_cards; }
-
-    int applyFilter(const QString &needle); // returns number of visible cards
-
-protected:
-    bool eventFilter(QObject *watched, QEvent *event) override;
-
-private:
-    QFrame *m_header = nullptr;
-    QLabel *m_arrow = nullptr;
-    QLabel *m_label = nullptr;
-    QWidget *m_content = nullptr;
-    QVBoxLayout *m_contentLayout = nullptr;
-    QList m_cards;
-    bool m_expanded = true;
-};
-
-class AgentSelectionDialog : public QDialog
-{
-    Q_OBJECT
-public:
-    AgentSelectionDialog(
-        const std::vector &configs,
-        const QString ¤tName,
-        AgentFactory *agentFactory = nullptr,
-        QWidget *parent = nullptr);
-
-    QString selectedName() const { return m_selectedName; }
-
-private:
-    void rebuild(const QString ¤tName);
-    void selectCard(ListRowCard *card);
-    void applyFilter(const QString &needle);
-
-    QLineEdit *m_filter = nullptr;
-    QScrollArea *m_scroll = nullptr;
-    QPushButton *m_okButton = nullptr;
-    ListRowCard *m_currentCard = nullptr;
-    QList m_sections;
-    QString m_selectedName;
-
-    AgentFactory *m_agentFactory = nullptr;
-    std::vector m_localConfigs; // fallback when no factory
-};
-
-} // namespace QodeAssist::Settings
diff --git a/sources/settings/AgentSlotWidget.cpp b/sources/settings/AgentSlotWidget.cpp
deleted file mode 100644
index 566e65d..0000000
--- a/sources/settings/AgentSlotWidget.cpp
+++ /dev/null
@@ -1,362 +0,0 @@
-// Copyright (C) 2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "AgentSlotWidget.hpp"
-
-#include "SettingsTr.hpp"
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace QodeAssist::Settings {
-
-namespace {
-
-bool isDarkPalette(const QPalette &p)
-{
-    return p.color(QPalette::Window).lightness() < 128;
-}
-
-QFont monoFont(int pixelSize)
-{
-    QFont f;
-    f.setFamilies({QStringLiteral("SF Mono"),
-                   QStringLiteral("Cascadia Code"),
-                   QStringLiteral("Consolas"),
-                   QStringLiteral("Liberation Mono"),
-                   QStringLiteral("Menlo"),
-                   QStringLiteral("Courier New")});
-    f.setStyleHint(QFont::Monospace);
-    f.setPixelSize(pixelSize);
-    return f;
-}
-
-struct Tone
-{
-    QString cardBg;
-    QString cardBd;
-    QString textSoft;
-    QString textMute;
-    QString textFaint;
-};
-
-Tone toneFor(bool dark)
-{
-    return dark ? Tone{"#333333", "#4a4a4a", "#c2c2c2", "#9a9a9a", "#7a7a7a"}
-                : Tone{"#f6f6f6", "#bdbdbd", "#3a3a3a", "#5a5a5a", "#8a8a8a"};
-}
-
-struct PillTone
-{
-    QString bg;
-    QString fg;
-    QString bd;
-};
-
-PillTone pillTone(Pill::Kind kind, bool dark)
-{
-    switch (kind) {
-    case Pill::Template:
-        return dark ? PillTone{"#2c3f5a", "#cfe1f7", "#4a6286"}
-                    : PillTone{"#dbe7f6", "#1f3f73", "#a8c1e0"};
-    case Pill::On:
-        return dark ? PillTone{"#2f4530", "#bce0bd", "#4a6c4b"}
-                    : PillTone{"#dbe9d3", "#2c5a1c", "#a3bc97"};
-    case Pill::Off:
-        return dark ? PillTone{"#3a3a3a", "#8a8a8a", "#4a4a4a"}
-                    : PillTone{"#ececec", "#7a7a7a", "#c8c8c8"};
-    case Pill::User:
-        return dark ? PillTone{"#4a3f24", "#e6cd92", "#6a5a30"}
-                    : PillTone{"#f0e4cf", "#75541a", "#cdb98a"};
-    case Pill::Tag:
-        return dark ? PillTone{"#2e2e3a", "#b9b9cf", "#46465a"}
-                    : PillTone{"#e7e7f2", "#46466e", "#c1c1d5"};
-    }
-    return {};
-}
-
-} // namespace
-
-// -- Pill --------------------------------------------------------------
-
-Pill::Pill(Kind kind, const QString &text, QWidget *parent)
-    : QLabel(text, parent)
-    , m_kind(kind)
-{
-    setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
-    setAlignment(Qt::AlignCenter);
-    QFont f = font();
-    f.setPixelSize(11);
-    setFont(f);
-    applyTheme();
-}
-
-void Pill::setKind(Kind kind)
-{
-    if (m_kind == kind)
-        return;
-    m_kind = kind;
-    applyTheme();
-}
-
-void Pill::changeEvent(QEvent *event)
-{
-    QLabel::changeEvent(event);
-    if (m_inApplyTheme)
-        return;
-    if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
-        applyTheme();
-}
-
-void Pill::applyTheme()
-{
-    if (m_inApplyTheme)
-        return;
-    QScopedValueRollback guard(m_inApplyTheme, true);
-
-    const auto t = pillTone(m_kind, isDarkPalette(palette()));
-    setStyleSheet(QStringLiteral(
-                      "QLabel { background-color: %1; color: %2;"
-                      " border: 1px solid %3; padding: 1px 7px; }")
-                      .arg(t.bg, t.fg, t.bd));
-}
-
-// -- AgentSlotWidget ---------------------------------------------------
-
-AgentSlotWidget::AgentSlotWidget(QWidget *parent)
-    : QWidget(parent)
-{
-    setAttribute(Qt::WA_StyledBackground, true);
-    setObjectName(QStringLiteral("AgentSlot"));
-
-    m_name = new QLabel(this);
-    QFont nameFont = m_name->font();
-    nameFont.setBold(true);
-    m_name->setFont(nameFont);
-    m_name->setTextInteractionFlags(Qt::TextSelectableByMouse);
-
-    m_sourcePill = new Pill(Pill::User, {}, this);
-    m_sourcePill->hide();
-
-    m_changeBtn = new QPushButton(Tr::tr("Change…"), this);
-    m_changeBtn->setCursor(Qt::PointingHandCursor);
-    connect(m_changeBtn, &QPushButton::clicked, this, &AgentSlotWidget::changeRequested);
-
-    m_editBtn = new QPushButton(Tr::tr("Edit"), this);
-    m_editBtn->setCursor(Qt::PointingHandCursor);
-    m_editBtn->setToolTip(Tr::tr("Open this agent in the Agents settings page."));
-    connect(m_editBtn, &QPushButton::clicked, this, &AgentSlotWidget::editRequested);
-
-    auto makeFieldLabel = [this]() {
-        auto *l = new QLabel(this);
-        l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
-        return l;
-    };
-    auto makeMonoValue = [this]() {
-        auto *l = new QLabel(this);
-        l->setFont(monoFont(11));
-        l->setTextInteractionFlags(Qt::TextSelectableByMouse);
-        l->setMinimumWidth(0);
-        l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
-        return l;
-    };
-    auto makePlainValue = [this]() {
-        auto *l = new QLabel(this);
-        l->setTextInteractionFlags(Qt::TextSelectableByMouse);
-        l->setMinimumWidth(0);
-        l->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
-        return l;
-    };
-
-    m_modelLabel = makeFieldLabel();
-    m_modelLabel->setText(Tr::tr("Model"));
-    m_modelValue = makeMonoValue();
-
-    m_urlLabel = makeFieldLabel();
-    m_urlLabel->setText(Tr::tr("Provider"));
-    m_urlValue = makeMonoValue();
-
-    m_endpointLabel = makeFieldLabel();
-    m_endpointLabel->setText(Tr::tr("Endpoint"));
-    m_endpointValue = makeMonoValue();
-
-    m_templateLabel = makeFieldLabel();
-    m_templateLabel->setText(Tr::tr("Template"));
-    m_templateValue = makePlainValue();
-
-    m_description = new QLabel(this);
-    m_description->setWordWrap(true);
-    m_description->setTextInteractionFlags(Qt::TextSelectableByMouse);
-    QFont descFont = m_description->font();
-    descFont.setItalic(true);
-    m_description->setFont(descFont);
-
-    m_thinkingPill = new Pill(Pill::Off, {}, this);
-    m_toolsPill = new Pill(Pill::Off, {}, this);
-
-    // Header row
-    auto *nameRow = new QHBoxLayout;
-    nameRow->setContentsMargins(0, 0, 0, 0);
-    nameRow->setSpacing(6);
-    nameRow->addWidget(m_name);
-    nameRow->addWidget(m_sourcePill);
-    nameRow->addStretch(1);
-
-    auto *buttonsBox = new QHBoxLayout;
-    buttonsBox->setContentsMargins(0, 0, 0, 0);
-    buttonsBox->setSpacing(4);
-    buttonsBox->addWidget(m_editBtn);
-    buttonsBox->addWidget(m_changeBtn);
-
-    auto *headerRow = new QHBoxLayout;
-    headerRow->setContentsMargins(0, 0, 0, 0);
-    headerRow->setSpacing(8);
-    headerRow->addLayout(nameRow, 1);
-    headerRow->addLayout(buttonsBox, 0);
-
-    // Two-column field grid
-    auto *grid = new QGridLayout;
-    grid->setContentsMargins(0, 0, 0, 0);
-    grid->setHorizontalSpacing(8);
-    grid->setVerticalSpacing(2);
-    grid->setColumnMinimumWidth(0, 62);
-    grid->setColumnStretch(0, 0);
-    grid->setColumnStretch(1, 1);
-
-    grid->addWidget(m_modelLabel, 0, 0);
-    grid->addWidget(m_modelValue, 0, 1);
-    grid->addWidget(m_urlLabel, 1, 0);
-    grid->addWidget(m_urlValue, 1, 1);
-    grid->addWidget(m_endpointLabel, 2, 0);
-    grid->addWidget(m_endpointValue, 2, 1);
-    grid->addWidget(m_templateLabel, 3, 0);
-    grid->addWidget(m_templateValue, 3, 1);
-
-    auto *pillsRow = new QHBoxLayout;
-    pillsRow->setContentsMargins(0, 0, 0, 0);
-    pillsRow->setSpacing(4);
-    pillsRow->addWidget(m_thinkingPill);
-    pillsRow->addWidget(m_toolsPill);
-    pillsRow->addStretch(1);
-
-    auto *outer = new QVBoxLayout(this);
-    outer->setContentsMargins(10, 10, 10, 10);
-    outer->setSpacing(6);
-    outer->addLayout(headerRow);
-    outer->addLayout(grid);
-    outer->addWidget(m_description);
-    outer->addLayout(pillsRow);
-
-    applyTheme();
-    clear();
-}
-
-void AgentSlotWidget::changeEvent(QEvent *event)
-{
-    QWidget::changeEvent(event);
-    if (m_inApplyTheme)
-        return;
-    if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange)
-        applyTheme();
-}
-
-void AgentSlotWidget::applyTheme()
-{
-    if (m_inApplyTheme)
-        return;
-    QScopedValueRollback guard(m_inApplyTheme, true);
-
-    const Tone t = toneFor(isDarkPalette(palette()));
-
-    setStyleSheet(QStringLiteral(
-                      "#AgentSlot { background-color: %1; border: 1px solid %2; }")
-                      .arg(t.cardBg, t.cardBd));
-
-    auto applyColor = [](QLabel *label, const QString &color) {
-        QPalette p = label->palette();
-        p.setColor(QPalette::WindowText, QColor(color));
-        label->setPalette(p);
-    };
-
-    for (QLabel *l : {m_modelLabel, m_urlLabel, m_endpointLabel, m_templateLabel})
-        applyColor(l, t.textMute);
-
-    applyColor(m_description, t.textSoft);
-}
-
-void AgentSlotWidget::setAgentConfig(const AgentConfig &cfg)
-{
-    m_name->setText(cfg.name);
-
-    if (cfg.isUserSource()) {
-        m_sourcePill->setText(cfg.overridesBundled
-                                  ? Tr::tr("User overrides bundled")
-                                  : Tr::tr("User"));
-        m_sourcePill->show();
-    } else {
-        m_sourcePill->hide();
-    }
-
-    m_modelValue->setText(cfg.model);
-    m_modelValue->setToolTip(cfg.model);
-    // The agent profile no longer carries a URL — the URL belongs to
-    // the referenced provider instance and is editable on the Providers
-    // settings page. Surface the instance name in this row instead.
-    m_urlValue->setText(cfg.providerInstance);
-    m_urlValue->setToolTip(cfg.providerInstance);
-    m_endpointValue->setText(cfg.endpoint);
-    m_endpointValue->setToolTip(cfg.endpoint);
-    // Templates are now inline in the agent profile — no separate name to
-    // surface. Keep the row but show '—' so the layout stays stable.
-    m_templateLabel->hide();
-    m_templateValue->hide();
-
-    m_description->setText(cfg.description.isEmpty()
-                               ? Tr::tr("No description provided.")
-                               : cfg.description);
-
-    const Tone t = toneFor(isDarkPalette(palette()));
-    QPalette descPal = m_description->palette();
-    descPal.setColor(QPalette::WindowText,
-                     QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft));
-    m_description->setPalette(descPal);
-
-    m_thinkingPill->setKind(cfg.enableThinking ? Pill::On : Pill::Off);
-    m_thinkingPill->setText(cfg.enableThinking ? Tr::tr("thinking on")
-                                               : Tr::tr("thinking off"));
-    m_toolsPill->setKind(cfg.enableTools ? Pill::On : Pill::Off);
-    m_toolsPill->setText(cfg.enableTools ? Tr::tr("tools on")
-                                         : Tr::tr("tools off"));
-    m_thinkingPill->show();
-    m_toolsPill->show();
-
-    m_editBtn->setEnabled(!cfg.name.isEmpty());
-}
-
-void AgentSlotWidget::clear()
-{
-    const Tone t = toneFor(isDarkPalette(palette()));
-
-    m_name->setText(QStringLiteral("%2")
-                        .arg(t.textFaint, Tr::tr("(no agent selected)")));
-    m_sourcePill->hide();
-
-    for (QLabel *l : {m_modelValue, m_urlValue, m_endpointValue, m_templateValue}) {
-        l->clear();
-        l->setToolTip({});
-    }
-
-    m_description->setText({});
-    m_thinkingPill->hide();
-    m_toolsPill->hide();
-    m_editBtn->setEnabled(false);
-}
-
-} // namespace QodeAssist::Settings
diff --git a/sources/settings/AgentSlotWidget.hpp b/sources/settings/AgentSlotWidget.hpp
deleted file mode 100644
index 285e88a..0000000
--- a/sources/settings/AgentSlotWidget.hpp
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) 2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-
-#include 
-
-class QPushButton;
-
-namespace QodeAssist::Settings {
-
-class Pill : public QLabel
-{
-    Q_OBJECT
-public:
-    enum Kind { Template, On, Off, User, Tag };
-
-    Pill(Kind kind, const QString &text = {}, QWidget *parent = nullptr);
-    void setKind(Kind kind);
-
-protected:
-    void changeEvent(QEvent *event) override;
-
-private:
-    void applyTheme();
-
-    Kind m_kind;
-    bool m_inApplyTheme = false;
-};
-
-class AgentSlotWidget : public QWidget
-{
-    Q_OBJECT
-public:
-    explicit AgentSlotWidget(QWidget *parent = nullptr);
-
-    void setAgentConfig(const AgentConfig &cfg);
-    void clear();
-
-    QPushButton *changeButton() const { return m_changeBtn; }
-
-signals:
-    void changeRequested();
-    void editRequested();
-
-protected:
-    void changeEvent(QEvent *event) override;
-
-private:
-    void applyTheme();
-
-    QLabel *m_name = nullptr;
-    Pill *m_sourcePill = nullptr;
-    QPushButton *m_changeBtn = nullptr;
-    QPushButton *m_editBtn = nullptr;
-
-    QLabel *m_modelLabel = nullptr;
-    QLabel *m_modelValue = nullptr;
-    QLabel *m_urlLabel = nullptr;
-    QLabel *m_urlValue = nullptr;
-    QLabel *m_endpointLabel = nullptr;
-    QLabel *m_endpointValue = nullptr;
-    QLabel *m_templateLabel = nullptr;
-    QLabel *m_templateValue = nullptr;
-
-    QLabel *m_description = nullptr;
-
-    Pill *m_thinkingPill = nullptr;
-    Pill *m_toolsPill = nullptr;
-
-    bool m_inApplyTheme = false;
-};
-
-} // namespace QodeAssist::Settings
diff --git a/sources/settings/CMakeLists.txt b/sources/settings/CMakeLists.txt
deleted file mode 100644
index 95d8660..0000000
--- a/sources/settings/CMakeLists.txt
+++ /dev/null
@@ -1,38 +0,0 @@
-add_library(QodeAssistAgentPipelines OBJECT
-    AgentPipelinesPage.hpp AgentPipelinesPage.cpp
-    PipelinesConfig.hpp PipelinesConfig.cpp
-    AgentRosterWidget.hpp AgentRosterWidget.cpp
-    AgentSlotWidget.hpp AgentSlotWidget.cpp
-    AgentSelectionDialog.hpp AgentSelectionDialog.cpp
-)
-
-target_include_directories(QodeAssistAgentPipelines PRIVATE
-    ${CMAKE_CURRENT_SOURCE_DIR}
-    ${CMAKE_SOURCE_DIR}/sources/providers
-    ${CMAKE_SOURCE_DIR}/sources/common
-    ${CMAKE_SOURCE_DIR}/sources/agents
-    ${CMAKE_SOURCE_DIR}/settings
-    ${CMAKE_SOURCE_DIR}/logger
-)
-
-target_link_libraries(QodeAssistAgentPipelines PRIVATE
-    Qt::Core
-    Qt::Gui
-    Qt::Widgets
-    Qt::Network
-    QtCreator::Core
-    QtCreator::Utils
-    Agents
-    Providers
-    Common
-    LLMQore
-    QodeAssistLogger
-    TomlSerializer
-    tomlplusplus::tomlplusplus
-)
-
-target_compile_definitions(QodeAssistAgentPipelines PRIVATE
-    QODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
-    QODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
-    QODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
-)
diff --git a/sources/settings/PipelinesConfig.cpp b/sources/settings/PipelinesConfig.cpp
deleted file mode 100644
index 72c79c1..0000000
--- a/sources/settings/PipelinesConfig.cpp
+++ /dev/null
@@ -1,281 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "PipelinesConfig.hpp"
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-
-#include "Logger.hpp"
-#include "TomlWriter.hpp"
-#include 
-
-namespace QodeAssist::Settings {
-
-namespace {
-
-constexpr const char *kSection = "pipelines";
-constexpr const char *kCodeCompletion = "code_completion";
-constexpr const char *kChatAssistant = "chat_assistant";
-constexpr const char *kChatCompression = "chat_compression";
-constexpr const char *kQuickRefactor = "quick_refactor";
-constexpr int kMaxAgentNameLen = 256;
-
-QString trimAndCap(const QString &raw)
-{
-    QString s = raw.trimmed();
-    if (s.size() > kMaxAgentNameLen)
-        s.truncate(kMaxAgentNameLen);
-    return s;
-}
-
-QStringList toStringList(const toml::node *node, const QString &slotKey, bool *schemaOk)
-{
-    QStringList out;
-    if (!node)
-        return out;
-    const auto *arr = node->as_array();
-    if (!arr) {
-        LOG_MESSAGE(QStringLiteral(
-            "[Pipelines] schema error: '%1' must be an array of strings, got non-array")
-                        .arg(slotKey));
-        if (schemaOk)
-            *schemaOk = false;
-        return out;
-    }
-    out.reserve(static_cast(arr->size()));
-    for (size_t i = 0; i < arr->size(); ++i) {
-        const auto &el = (*arr)[i];
-        if (const auto *s = el.as_string()) {
-            const QString name = trimAndCap(QString::fromStdString(s->get()));
-            if (name.isEmpty())
-                continue;
-            if (out.contains(name)) {
-                LOG_MESSAGE(QStringLiteral("[Pipelines] '%1' element #%2 is a duplicate, dropping")
-                                .arg(slotKey)
-                                .arg(i));
-                continue;
-            }
-            out.append(name);
-        } else {
-            LOG_MESSAGE(QStringLiteral("[Pipelines] '%1' element #%2 is not a string, dropping")
-                            .arg(slotKey)
-                            .arg(i));
-            if (schemaOk)
-                *schemaOk = false;
-        }
-    }
-    return out;
-}
-
-void fillMissingFromDefaults(PipelineRosters &r, const toml::table §ion)
-{
-    const PipelineRosters defs = PipelineRosters::defaults();
-    if (!section.contains(kCodeCompletion))
-        r.codeCompletion = defs.codeCompletion;
-    if (!section.contains(kChatAssistant))
-        r.chatAssistant = defs.chatAssistant;
-    if (!section.contains(kChatCompression))
-        r.chatCompression = defs.chatCompression;
-    if (!section.contains(kQuickRefactor))
-        r.quickRefactor = defs.quickRefactor;
-}
-
-} // namespace
-
-PipelineRosters PipelineRosters::defaults()
-{
-    PipelineRosters r;
-    r.codeCompletion = {QStringLiteral("Ollama Qwen2.5-Coder Completion")};
-    r.chatAssistant = {QStringLiteral("Ollama Chat")};
-    r.chatCompression = {QStringLiteral("Ollama Compression")};
-    r.quickRefactor = {QStringLiteral("Ollama Quick Refactor")};
-    return r;
-}
-
-QString PipelinesConfig::filePath()
-{
-    return Core::ICore::userResourcePath(QStringLiteral("qodeassist/config/pipelines.toml"))
-        .toFSPathString();
-}
-
-PipelinesLoadResult PipelinesConfig::load()
-{
-    PipelinesLoadResult result;
-    const QString path = filePath();
-    QFile f(path);
-    if (!f.exists()) {
-        result.rosters = PipelineRosters::defaults();
-        result.status = PipelinesLoadStatus::FileMissing;
-        return result;
-    }
-    if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
-        result.rosters = PipelineRosters::defaults();
-        result.status = PipelinesLoadStatus::ParseError;
-        result.message = QStringLiteral("cannot open %1: %2").arg(path, f.errorString());
-        LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
-        return result;
-    }
-    const QByteArray contents = f.readAll();
-    f.close();
-
-    toml::table tbl;
-    try {
-        tbl = toml::parse(std::string_view(contents.constData(),
-                                            static_cast(contents.size())),
-                          path.toStdString());
-    } catch (const toml::parse_error &e) {
-        result.rosters = PipelineRosters::defaults();
-        result.status = PipelinesLoadStatus::ParseError;
-        result.message = QStringLiteral("parse error in %1: %2")
-                             .arg(path, QString::fromStdString(std::string(e.description())));
-        LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
-        return result;
-    } catch (const std::exception &e) {
-        result.rosters = PipelineRosters::defaults();
-        result.status = PipelinesLoadStatus::ParseError;
-        result.message = QStringLiteral("error reading %1: %2")
-                             .arg(path, QString::fromUtf8(e.what()));
-        LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
-        return result;
-    } catch (...) {
-        result.rosters = PipelineRosters::defaults();
-        result.status = PipelinesLoadStatus::ParseError;
-        result.message = QStringLiteral("unknown error reading %1").arg(path);
-        LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(result.message));
-        return result;
-    }
-
-    const auto *section = tbl[kSection].as_table();
-    if (!section) {
-        LOG_MESSAGE(QStringLiteral("[Pipelines] no [pipelines] section in %1; using defaults")
-                        .arg(path));
-        result.rosters = PipelineRosters::defaults();
-        result.status = PipelinesLoadStatus::SchemaError;
-        result.message = QStringLiteral("missing [pipelines] section");
-        return result;
-    }
-
-    bool schemaOk = true;
-    result.rosters.codeCompletion
-        = toStringList((*section)[kCodeCompletion].node(), kCodeCompletion, &schemaOk);
-    result.rosters.chatAssistant
-        = toStringList((*section)[kChatAssistant].node(), kChatAssistant, &schemaOk);
-    result.rosters.chatCompression
-        = toStringList((*section)[kChatCompression].node(), kChatCompression, &schemaOk);
-    result.rosters.quickRefactor
-        = toStringList((*section)[kQuickRefactor].node(), kQuickRefactor, &schemaOk);
-
-    fillMissingFromDefaults(result.rosters, *section);
-
-    if (!schemaOk) {
-        result.status = PipelinesLoadStatus::SchemaError;
-        result.message = QStringLiteral("some entries had wrong type; see log");
-    }
-    return result;
-}
-
-bool PipelinesConfig::save(const PipelineRosters &rosters, QString *errorOut)
-{
-    const QString path = filePath();
-    const QFileInfo info(path);
-    if (!QDir().mkpath(info.absolutePath())) {
-        const QString err = QStringLiteral("cannot create configuration directory: %1")
-                                .arg(info.absolutePath());
-        LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(err));
-        if (errorOut)
-            *errorOut = err;
-        return false;
-    }
-
-    TomlSerializer::TomlWriter w;
-    w.writeComment(QStringLiteral(
-        "QodeAssist pipelines — slot → ordered list of agent names."));
-    w.writeComment(QStringLiteral(
-        "The router walks each list top-down at request time and uses"));
-    w.writeComment(QStringLiteral("the first matching agent."));
-    w.writeBlankLine();
-    w.writeTableHeader(QString::fromUtf8(kSection));
-    w.writeStringArray(QString::fromUtf8(kCodeCompletion), rosters.codeCompletion);
-    w.writeStringArray(QString::fromUtf8(kChatAssistant), rosters.chatAssistant);
-    w.writeStringArray(QString::fromUtf8(kChatCompression), rosters.chatCompression);
-    w.writeStringArray(QString::fromUtf8(kQuickRefactor), rosters.quickRefactor);
-
-    QSaveFile out(path);
-    if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
-        if (errorOut)
-            *errorOut = out.errorString();
-        return false;
-    }
-    const QByteArray payload = w.toUtf8();
-    const qint64 written = out.write(payload);
-    if (written != payload.size()) {
-        const QString err = QStringLiteral("short write (%1/%2): %3")
-                                .arg(written)
-                                .arg(payload.size())
-                                .arg(out.errorString());
-        LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(err));
-        out.cancelWriting();
-        if (errorOut)
-            *errorOut = err;
-        return false;
-    }
-    if (!out.commit()) {
-        if (errorOut)
-            *errorOut = out.errorString();
-        return false;
-    }
-    return true;
-}
-
-bool PipelinesConfig::validate(const QodeAssist::AgentFactory &factory, QString *errorOut)
-{
-    PipelinesLoadResult lr = load();
-    PipelineRosters &r = lr.rosters;
-    bool changed = false;
-
-    auto fix = [&](QStringList ¤t) {
-        QStringList kept;
-        kept.reserve(current.size());
-        for (const QString &raw : current) {
-            const QString name = trimAndCap(raw);
-            if (name.isEmpty() || kept.contains(name))
-                continue;
-            if (factory.configByName(name))
-                kept.append(name);
-        }
-        if (kept != current) {
-            current = std::move(kept);
-            changed = true;
-        }
-    };
-
-    fix(r.codeCompletion);
-    fix(r.chatAssistant);
-    fix(r.chatCompression);
-    fix(r.quickRefactor);
-
-    if (!changed && lr.status == PipelinesLoadStatus::Ok)
-        return true;
-
-    QString saveErr;
-    if (!save(r, &saveErr)) {
-        const QString msg = QStringLiteral("failed to persist after validation: %1").arg(saveErr);
-        LOG_MESSAGE(QStringLiteral("[Pipelines] %1").arg(msg));
-        if (errorOut)
-            *errorOut = msg;
-        return false;
-    }
-    return true;
-}
-
-} // namespace QodeAssist::Settings
diff --git a/sources/settings/PipelinesConfig.hpp b/sources/settings/PipelinesConfig.hpp
deleted file mode 100644
index b5760ad..0000000
--- a/sources/settings/PipelinesConfig.hpp
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-
-namespace QodeAssist {
-class AgentFactory;
-}
-
-namespace QodeAssist::Settings {
-
-struct PipelineRosters
-{
-    QStringList codeCompletion;
-    QStringList chatAssistant;
-    QStringList chatCompression;
-    QStringList quickRefactor;
-
-    [[nodiscard]] static PipelineRosters defaults();
-};
-
-enum class PipelinesLoadStatus { Ok, FileMissing, ParseError, SchemaError };
-
-struct PipelinesLoadResult
-{
-    PipelineRosters rosters;
-    PipelinesLoadStatus status = PipelinesLoadStatus::Ok;
-    QString message;
-};
-
-class PipelinesConfig
-{
-public:
-    [[nodiscard]] static QString filePath();
-
-    [[nodiscard]] static PipelinesLoadResult load();
-
-    [[nodiscard]] static bool save(const PipelineRosters &rosters, QString *errorOut = nullptr);
-
-    [[nodiscard]] static bool validate(
-        const QodeAssist::AgentFactory &factory, QString *errorOut = nullptr);
-};
-
-} // namespace QodeAssist::Settings
diff --git a/sources/templates/CMakeLists.txt b/sources/templates/CMakeLists.txt
deleted file mode 100644
index 4a7c81d..0000000
--- a/sources/templates/CMakeLists.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-add_library(Templates STATIC
-    PromptTemplate.hpp
-    JsonPromptTemplate.hpp JsonPromptTemplate.cpp
-)
-
-target_link_libraries(Templates
-    PUBLIC
-        Qt::Core
-        Common
-        Providers
-        pantor::inja
-)
-
-target_include_directories(Templates
-    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
-    PRIVATE ${CMAKE_SOURCE_DIR}/sources/agents
-)
diff --git a/sources/templates/JsonPromptTemplate.cpp b/sources/templates/JsonPromptTemplate.cpp
deleted file mode 100644
index f45543c..0000000
--- a/sources/templates/JsonPromptTemplate.cpp
+++ /dev/null
@@ -1,337 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "JsonPromptTemplate.hpp"
-
-#include 
-#include 
-#include 
-
-#include 
-
-#include "AgentConfig.hpp"
-
-namespace QodeAssist::Templates {
-
-namespace {
-
-nlohmann::json buildContextJson(const ContextData &context)
-{
-    nlohmann::json ctx = nlohmann::json::object();
-
-    if (context.systemPrompt) {
-        ctx["system_prompt"] = context.systemPrompt->toStdString();
-    }
-
-    if (context.prefix) {
-        ctx["prefix"] = context.prefix->toStdString();
-    }
-    if (context.suffix) {
-        ctx["suffix"] = context.suffix->toStdString();
-    }
-
-    if (context.filesMetadata && !context.filesMetadata->isEmpty()) {
-        nlohmann::json files = nlohmann::json::array();
-        for (const auto &file : context.filesMetadata.value()) {
-            nlohmann::json fj = nlohmann::json::object();
-            fj["file_path"] = file.filePath.toStdString();
-            fj["content"] = file.content.toStdString();
-            files.push_back(std::move(fj));
-        }
-        ctx["files_metadata"] = std::move(files);
-    }
-
-    nlohmann::json history = nlohmann::json::array();
-    if (context.history) {
-        for (const auto &msg : context.history.value()) {
-            nlohmann::json mj = nlohmann::json::object();
-            mj["role"] = msg.role.toStdString();
-
-            nlohmann::json blocks = nlohmann::json::array();
-            QString flatContent;
-            nlohmann::json flatImages = nlohmann::json::array();
-
-            for (const auto &b : msg.blocks) {
-                nlohmann::json bj = nlohmann::json::object();
-                switch (b.kind) {
-                case ContentBlockEntry::Kind::Text:
-                    bj["type"] = "text";
-                    bj["text"] = b.text.toStdString();
-                    flatContent += b.text;
-                    break;
-                case ContentBlockEntry::Kind::Thinking:
-                    bj["type"] = "thinking";
-                    bj["thinking"] = b.thinking.toStdString();
-                    bj["signature"] = b.signature.toStdString();
-                    break;
-                case ContentBlockEntry::Kind::RedactedThinking:
-                    bj["type"] = "redacted_thinking";
-                    bj["data"] = b.signature.toStdString();
-                    break;
-                case ContentBlockEntry::Kind::ToolUse: {
-                    bj["type"] = "tool_use";
-                    bj["id"] = b.toolUseId.toStdString();
-                    bj["name"] = b.toolName.toStdString();
-                    const std::string inputStr
-                        = QJsonDocument(b.toolInput).toJson(QJsonDocument::Compact).toStdString();
-                    nlohmann::json parsedInput
-                        = nlohmann::json::parse(inputStr, nullptr, /*allow_exceptions=*/false);
-                    if (parsedInput.is_discarded()) {
-                        if (!b.toolInput.isEmpty()) {
-                            qWarning("[QodeAssist] tool_use '%s' has unparseable input "
-                                     "(serialized as null): %s",
-                                     qUtf8Printable(b.toolName),
-                                     inputStr.c_str());
-                        }
-                        parsedInput = nullptr;
-                    }
-                    bj["input"] = std::move(parsedInput);
-                    break;
-                }
-                case ContentBlockEntry::Kind::ToolResult:
-                    bj["type"] = "tool_result";
-                    bj["tool_use_id"] = b.toolUseId.toStdString();
-                    bj["content"] = b.result.toStdString();
-                    break;
-                case ContentBlockEntry::Kind::Image:
-                    bj["type"] = "image";
-                    bj["data"] = b.imageData.toStdString();
-                    bj["media_type"] = b.mediaType.toStdString();
-                    bj["is_url"] = b.isImageUrl;
-                    {
-                        nlohmann::json ij = nlohmann::json::object();
-                        ij["data"] = b.imageData.toStdString();
-                        ij["media_type"] = b.mediaType.toStdString();
-                        ij["is_url"] = b.isImageUrl;
-                        flatImages.push_back(std::move(ij));
-                    }
-                    break;
-                }
-                blocks.push_back(std::move(bj));
-            }
-
-            mj["content"] = flatContent.toStdString();
-            if (!flatImages.empty())
-                mj["images"] = std::move(flatImages);
-            mj["content_blocks"] = std::move(blocks);
-
-            history.push_back(std::move(mj));
-        }
-    }
-    ctx["history"] = std::move(history);
-
-    nlohmann::json data = nlohmann::json::object();
-    data["ctx"] = std::move(ctx);
-    return data;
-}
-
-void registerStandardCallbacks(inja::Environment &env)
-{
-    // Sandbox: disable filesystem reads from `{% include %}` and reject
-    // any include callback. User-authored templates run with full
-    // process privileges, so they must not slurp arbitrary files via
-    // include directives. File reads happen only through
-    // ContextManager-provided callbacks (e.g. read_file()).
-    env.set_search_included_templates_in_files(false);
-    env.set_include_callback(
-        [](const std::filesystem::path &, const std::string &name) -> inja::Template {
-            throw inja::FileError(
-                "include is disabled in QodeAssist templates: '" + name + "'");
-        });
-
-    // Disable inja's `##` line-statement shorthand — collides with
-    // Markdown headings inside template bodies. Same rationale as in
-    // ContextRenderer; retarget to an unreachable sentinel.
-    env.set_line_statement("@@@inja@@@");
-
-    env.add_callback("tojson", 1, [](inja::Arguments &args) -> nlohmann::json {
-        return args.at(0)->dump();
-    });
-
-    env.add_callback("strip_signature_suffix", 1, [](inja::Arguments &args) -> nlohmann::json {
-        std::string content = args.at(0)->get();
-        const std::string marker = "\n[Signature: ";
-        const auto pos = content.find(marker);
-        if (pos != std::string::npos) {
-            content = content.substr(0, pos);
-        }
-        return content;
-    });
-
-    env.add_callback("filter_skip_role", 2, [](inja::Arguments &args) -> nlohmann::json {
-        const nlohmann::json &history = *args.at(0);
-        const std::string role = args.at(1)->get();
-        nlohmann::json result = nlohmann::json::array();
-        for (const auto &msg : history) {
-            if (msg.contains("role") && msg["role"].get() == role) {
-                continue;
-            }
-            result.push_back(msg);
-        }
-        return result;
-    });
-
-    env.add_callback("filter_skip_empty_thinking", 1, [](inja::Arguments &args) -> nlohmann::json {
-        const nlohmann::json &history = *args.at(0);
-        nlohmann::json result = nlohmann::json::array();
-        for (const auto &msg : history) {
-            const bool isThinking = msg.value("is_thinking", false);
-            const std::string sig = msg.value("signature", "");
-            if (isThinking && sig.empty()) {
-                continue;
-            }
-            result.push_back(msg);
-        }
-        return result;
-    });
-
-    env.add_callback(
-        "filter_skip_empty_parts_thinking", 1, [](inja::Arguments &args) -> nlohmann::json {
-            const nlohmann::json &history = *args.at(0);
-            nlohmann::json result = nlohmann::json::array();
-            for (const auto &msg : history) {
-                const bool isThinking = msg.value("is_thinking", false);
-                const std::string content = msg.value("content", "");
-                const std::string sig = msg.value("signature", "");
-                if (isThinking && content.empty() && sig.empty()) {
-                    continue;
-                }
-                result.push_back(msg);
-            }
-            return result;
-        });
-}
-
-} // namespace
-
-std::unique_ptr JsonPromptTemplate::fromConfig(
-    const AgentConfig &cfg, QString *error)
-{
-    auto setError = [&error](const QString &msg) {
-        if (error) *error = msg;
-    };
-
-    if (cfg.messageFormat.isEmpty()) {
-        setError(QStringLiteral("Agent '%1' has empty message_format").arg(cfg.name));
-        return nullptr;
-    }
-
-    auto tpl = std::unique_ptr(new JsonPromptTemplate);
-    tpl->m_name = cfg.name;
-    tpl->m_description = cfg.description;
-    tpl->m_sampling = cfg.sampling;
-    tpl->m_thinking = cfg.thinking;
-
-    registerStandardCallbacks(tpl->m_env);
-    try {
-        tpl->m_template = tpl->m_env.parse(cfg.messageFormat.toStdString());
-    } catch (const std::exception &e) {
-        setError(QStringLiteral("Failed to parse jinja for '%1': %2")
-                     .arg(cfg.name, QString::fromUtf8(e.what())));
-        return nullptr;
-    }
-    return tpl;
-}
-
-std::optional JsonPromptTemplate::renderBody(const ContextData &context) const
-{
-    const nlohmann::json data = buildContextJson(context);
-
-    std::string rendered;
-    try {
-        std::lock_guard lock(m_renderMutex);
-        rendered = m_env.render(m_template, data);
-    } catch (const std::exception &e) {
-        qWarning("[QodeAssist] Template '%s' render failed: %s",
-                 qUtf8Printable(m_name),
-                 e.what());
-        return std::nullopt;
-    }
-
-    QJsonParseError err;
-    const QJsonDocument doc
-        = QJsonDocument::fromJson(QByteArray::fromStdString(rendered), &err);
-    constexpr std::size_t kMaxRenderedLogChars = 500;
-    const std::string truncated = rendered.size() > kMaxRenderedLogChars
-        ? rendered.substr(0, kMaxRenderedLogChars) + "... [truncated]"
-        : rendered;
-    if (err.error != QJsonParseError::NoError) {
-        qWarning("[QodeAssist] Template '%s' produced invalid JSON at offset %d: %s\n"
-                 "--- raw output (truncated) ---\n%s",
-                 qUtf8Printable(m_name),
-                 err.offset,
-                 qUtf8Printable(err.errorString()),
-                 truncated.c_str());
-        return std::nullopt;
-    }
-    if (!doc.isObject()) {
-        qWarning("[QodeAssist] Template '%s' rendered a non-object JSON value (truncated):\n%s",
-                 qUtf8Printable(m_name),
-                 truncated.c_str());
-        return std::nullopt;
-    }
-    return doc.object();
-}
-
-namespace {
-
-bool mergeRenderedBody(QJsonObject &request, const std::optional &body)
-{
-    if (!body)
-        return false;
-    for (auto it = body->constBegin(); it != body->constEnd(); ++it) {
-        request.insert(it.key(), it.value());
-    }
-    return true;
-}
-
-void deepMergeInto(QJsonObject &base, const QJsonObject &overlay)
-{
-    for (auto it = overlay.constBegin(); it != overlay.constEnd(); ++it) {
-        const QJsonValue baseVal = base.value(it.key());
-        const QJsonValue overlayVal = it.value();
-        if (baseVal.isObject() && overlayVal.isObject()) {
-            QJsonObject merged = baseVal.toObject();
-            deepMergeInto(merged, overlayVal.toObject());
-            base[it.key()] = merged;
-        } else {
-            base[it.key()] = overlayVal;
-        }
-    }
-}
-
-} // namespace
-
-void JsonPromptTemplate::prepareRequest(QJsonObject &request, const ContextData &context) const
-{
-    mergeRenderedBody(request, renderBody(context));
-}
-
-bool JsonPromptTemplate::buildFullRequest(
-    QJsonObject &request,
-    const ContextData &context,
-    bool thinkingEnabled) const
-{
-    if (!mergeRenderedBody(request, renderBody(context)))
-        return false;
-    applySampling(request, thinkingEnabled);
-    return true;
-}
-
-void JsonPromptTemplate::applySampling(QJsonObject &request, bool thinkingEnabled) const
-{
-    // Merge order: sampling provides defaults → body wins for its own
-    // keys → thinking overrides win on top.
-    QJsonObject merged = m_sampling;
-    deepMergeInto(merged, request);
-
-    if (thinkingEnabled && !m_thinking.isEmpty()) {
-        deepMergeInto(merged, m_thinking.value("overrides").toObject());
-        deepMergeInto(merged, m_thinking.value("request_block").toObject());
-    }
-
-    request = std::move(merged);
-}
-
-} // namespace QodeAssist::Templates
diff --git a/sources/templates/JsonPromptTemplate.hpp b/sources/templates/JsonPromptTemplate.hpp
deleted file mode 100644
index b38a262..0000000
--- a/sources/templates/JsonPromptTemplate.hpp
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-
-#include 
-#include 
-
-#include 
-
-#include "PromptTemplate.hpp"
-
-namespace QodeAssist {
-struct AgentConfig;
-}
-
-namespace QodeAssist::Templates {
-
-// Renderer for the request-body jinja template embedded in an
-// AgentConfig. One per Agent — built inline from the config (no shared
-// template registry, no model/provider filtering).
-class JsonPromptTemplate : public PromptTemplate
-{
-public:
-    // Build a renderer from an already-parsed agent config. Compiles
-    // the jinja source via inja once. On failure returns nullptr and
-    // populates `*error` (existing content preserved; pass nullptr to
-    // discard).
-    static std::unique_ptr fromConfig(
-        const AgentConfig &cfg, QString *error = nullptr);
-
-    QString name() const override { return m_name; }
-    QString description() const override { return m_description; }
-
-    // Standalone-template filters are gone — each template is built for
-    // exactly one agent, so it always matches its owner's provider/model.
-    bool isSupportProvider(Providers::ProviderID) const override { return true; }
-    bool isSupportModel(const QString &) const override { return true; }
-    PromptShape shape() const override { return PromptShape::Chat; }
-
-    void prepareRequest(QJsonObject &request, const ContextData &context) const override;
-
-    [[nodiscard]] bool buildFullRequest(
-        QJsonObject &request,
-        const ContextData &context,
-        bool thinkingEnabled = false) const override;
-
-    const QJsonObject &sampling() const { return m_sampling; }
-
-private:
-    JsonPromptTemplate() = default;
-
-    std::optional renderBody(const ContextData &context) const;
-    void applySampling(QJsonObject &request, bool thinkingEnabled) const;
-
-    QString m_name;
-    QString m_description;
-
-    // m_env is populated once in fromConfig() and never mutated again.
-    // It is `mutable` only because inja::Environment::render() is not a
-    // const member; m_renderMutex serialises those render() calls since
-    // inja's render path is not internally re-entrant on one Environment.
-    mutable inja::Environment m_env;
-    inja::Template m_template;
-    mutable std::mutex m_renderMutex;
-
-    QJsonObject m_sampling;
-    QJsonObject m_thinking;
-};
-
-} // namespace QodeAssist::Templates
diff --git a/sources/templates/PromptTemplate.hpp b/sources/templates/PromptTemplate.hpp
deleted file mode 100644
index ad39ef0..0000000
--- a/sources/templates/PromptTemplate.hpp
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-
-#include "ContextData.hpp"
-#include "ProviderID.hpp"
-
-namespace QodeAssist::Templates {
-
-using Providers::ProviderID;
-
-enum class PromptShape {
-    Chat,
-    Fim,
-};
-
-class PromptTemplate
-{
-public:
-    PromptTemplate() = default;
-    virtual ~PromptTemplate() = default;
-
-    PromptTemplate(const PromptTemplate &) = delete;
-    PromptTemplate &operator=(const PromptTemplate &) = delete;
-    PromptTemplate(PromptTemplate &&) = delete;
-    PromptTemplate &operator=(PromptTemplate &&) = delete;
-
-    virtual QString name() const = 0;
-    virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0;
-    virtual QString description() const = 0;
-    virtual bool isSupportProvider(ProviderID id) const = 0;
-    virtual PromptShape shape() const { return PromptShape::Chat; }
-
-    virtual bool isSupportModel(const QString & /*modelName*/) const { return true; }
-
-    [[nodiscard]] virtual bool buildFullRequest(
-        QJsonObject &request,
-        const ContextData &context,
-        bool /*thinkingEnabled*/ = false) const
-    {
-        prepareRequest(request, context);
-        return true;
-    }
-};
-} // namespace QodeAssist::Templates
diff --git a/sources/tomlSerializer/CMakeLists.txt b/sources/tomlSerializer/CMakeLists.txt
deleted file mode 100644
index a4a7a16..0000000
--- a/sources/tomlSerializer/CMakeLists.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-add_library(TomlSerializer STATIC
-    TomlWriter.hpp TomlWriter.cpp
-)
-
-target_link_libraries(TomlSerializer
-    PUBLIC
-        Qt::Core
-)
-
-target_include_directories(TomlSerializer
-    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
-)
diff --git a/sources/tomlSerializer/TomlWriter.cpp b/sources/tomlSerializer/TomlWriter.cpp
deleted file mode 100644
index 3cc9b4f..0000000
--- a/sources/tomlSerializer/TomlWriter.cpp
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include "TomlWriter.hpp"
-
-#include 
-
-namespace QodeAssist::TomlSerializer {
-
-QString escapeBasic(const QString &s)
-{
-    QString out;
-    out.reserve(s.size());
-    for (QChar c : s) {
-        const ushort u = c.unicode();
-        switch (u) {
-        case '\\': out += QLatin1String("\\\\"); break;
-        case '"':  out += QLatin1String("\\\""); break;
-        case '\b': out += QLatin1String("\\b"); break;
-        case '\t': out += QLatin1String("\\t"); break;
-        case '\n': out += QLatin1String("\\n"); break;
-        case '\f': out += QLatin1String("\\f"); break;
-        case '\r': out += QLatin1String("\\r"); break;
-        default:
-            if (u < 0x20 || u == 0x7f)
-                out += QStringLiteral("\\u%1").arg(u, 4, 16, QLatin1Char('0'));
-            else
-                out += c;
-            break;
-        }
-    }
-    return out;
-}
-
-void TomlWriter::writeBlankLine()
-{
-    m_out += QLatin1Char('\n');
-}
-
-void TomlWriter::writeComment(const QString &line)
-{
-    m_out += QLatin1String("# ");
-    m_out += line;
-    m_out += QLatin1Char('\n');
-}
-
-void TomlWriter::writeTableHeader(const QString &name)
-{
-    m_out += QLatin1Char('[');
-    m_out += name;
-    m_out += QLatin1String("]\n");
-}
-
-void TomlWriter::writeKeyPrefix(const QString &key)
-{
-    m_out += key;
-    if (m_keyColumnWidth > key.size())
-        m_out += QString(m_keyColumnWidth - key.size(), QLatin1Char(' '));
-    m_out += QLatin1String(" = ");
-}
-
-void TomlWriter::writeString(const QString &key, const QString &value)
-{
-    writeKeyPrefix(key);
-    m_out += QLatin1Char('"');
-    m_out += escapeBasic(value);
-    m_out += QLatin1String("\"\n");
-}
-
-void TomlWriter::writeBool(const QString &key, bool value)
-{
-    writeKeyPrefix(key);
-    m_out += value ? QLatin1String("true") : QLatin1String("false");
-    m_out += QLatin1Char('\n');
-}
-
-void TomlWriter::writeInt(const QString &key, qint64 value)
-{
-    writeKeyPrefix(key);
-    m_out += QString::number(value);
-    m_out += QLatin1Char('\n');
-}
-
-void TomlWriter::writeDouble(const QString &key, double value)
-{
-    writeKeyPrefix(key);
-    m_out += QString::number(value);
-    m_out += QLatin1Char('\n');
-}
-
-void TomlWriter::writeStringArray(const QString &key, const QStringList &values)
-{
-    writeKeyPrefix(key);
-    m_out += QLatin1Char('[');
-    bool first = true;
-    for (const QString &v : values) {
-        if (!first)
-            m_out += QLatin1String(", ");
-        m_out += QLatin1Char('"');
-        m_out += escapeBasic(v);
-        m_out += QLatin1Char('"');
-        first = false;
-    }
-    m_out += QLatin1String("]\n");
-}
-
-void TomlWriter::writeJsonPrimitives(const QJsonObject &obj)
-{
-    for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) {
-        const QJsonValue &v = it.value();
-        switch (v.type()) {
-        case QJsonValue::String: writeString(it.key(), v.toString()); break;
-        case QJsonValue::Bool:   writeBool(it.key(), v.toBool()); break;
-        case QJsonValue::Double: {
-            const double d = v.toDouble();
-            const qint64 i = static_cast(d);
-            if (static_cast(i) == d)
-                writeInt(it.key(), i);
-            else
-                writeDouble(it.key(), d);
-            break;
-        }
-        default:
-            break;
-        }
-    }
-}
-
-void TomlWriter::writeStringDict(const QHash &dict)
-{
-    for (auto it = dict.constBegin(); it != dict.constEnd(); ++it)
-        writeString(it.key(), it.value());
-}
-
-} // namespace QodeAssist::TomlSerializer
diff --git a/sources/tomlSerializer/TomlWriter.hpp b/sources/tomlSerializer/TomlWriter.hpp
deleted file mode 100644
index 922369e..0000000
--- a/sources/tomlSerializer/TomlWriter.hpp
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-
-namespace QodeAssist::TomlSerializer {
-
-[[nodiscard]] QString escapeBasic(const QString &s);
-
-class TomlWriter
-{
-public:
-    TomlWriter() = default;
-    explicit TomlWriter(int keyColumnWidth) : m_keyColumnWidth(keyColumnWidth) {}
-
-    void setKeyColumnWidth(int width) { m_keyColumnWidth = width; }
-
-    void writeBlankLine();
-    void writeComment(const QString &line);     // "# line\n"
-    void writeTableHeader(const QString &name); // "[name]\n"
-
-    void writeString(const QString &key, const QString &value);
-    void writeBool(const QString &key, bool value);
-    void writeInt(const QString &key, qint64 value);
-    void writeDouble(const QString &key, double value);
-    void writeStringArray(const QString &key, const QStringList &values);
-
-    void writeJsonPrimitives(const QJsonObject &obj);
-
-    void writeStringDict(const QHash &dict);
-
-    [[nodiscard]] QString result() const { return m_out; }
-    [[nodiscard]] QByteArray toUtf8() const { return m_out.toUtf8(); }
-
-private:
-    void writeKeyPrefix(const QString &key);
-
-    QString m_out;
-    int m_keyColumnWidth = 0;
-};
-
-} // namespace QodeAssist::TomlSerializer