refactor: Remove all experimental code

This commit is contained in:
Petr Mironychev
2026-07-13 09:37:04 +02:00
parent 34ce787320
commit adef7972ed
145 changed files with 0 additions and 13468 deletions

View File

@@ -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

View File

@@ -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
)

View File

@@ -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}
)

View File

@@ -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

View File

@@ -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 <QQuickItem>
#include "FlowsModel.hpp"
#include <FlowManager.hpp>
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

View File

@@ -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<TaskItem *>(child);
m_taskItemsList.insert(taskItem, taskItem->task());
}
if (child->objectName() == QString("TaskConnectionItem")) {
qDebug() << "Found TaskConnectionItem:" << child;
auto connectionItem = qobject_cast<TaskConnectionItem *>(child);
m_taskConnectionsList.insert(connectionItem, connectionItem->connection());
}
}
}
} // namespace QodeAssist::TaskFlow

View File

@@ -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 <QQuickItem>
#include "TaskConnectionItem.hpp"
#include "TaskConnectionsModel.hpp"
#include "TaskItem.hpp"
#include "TaskModel.hpp"
#include <Flow.hpp>
#include <TaskConnection.hpp>
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<TaskItem *, BaseTask *> m_taskItemsList;
QHash<TaskConnectionItem *, TaskConnection *> m_taskConnectionsList;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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<int, QByteArray> FlowsModel::roleNames() const
{
QHash<int, QByteArray> 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

View File

@@ -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 <QAbstractListModel>
#include <QObject>
// #include "tasks/Flow.hpp"
#include <FlowManager.hpp>
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<int, QByteArray> roleNames() const override;
public slots:
void onFlowAdded(const QString &flowId);
void onFlowRemoved(const QString &flowId);
private:
FlowManager *m_flowManager;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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 <QPainter>
#include <QPixmap>
#include <QQuickWindow>
#include <QSGSimpleRectNode>
#include <QSGSimpleTextureNode>
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<QSGSimpleTextureNode *>(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

View File

@@ -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 <QColor>
#include <QPainter>
#include <QQuickItem>
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

View File

@@ -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 <QDebug>
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<QQuickItem *>(item);
if (!taskItem)
continue;
QVariant taskProp = taskItem->property("task");
if (taskProp.isValid() && taskProp.value<BaseTask *>() == 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<BaseTask *>();
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<QQuickItem *(QQuickItem *)> findPortRecursive =
[&](QQuickItem *item) -> QQuickItem * {
// Проверяем objectName и port property
if (item->objectName() == "TaskPortItem") {
QVariant portProp = item->property("port");
if (portProp.isValid()) {
TaskPort *itemPort = portProp.value<TaskPort *>();
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

View File

@@ -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 <QPointF>
#include <QQuickItem>
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

View File

@@ -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<int, QByteArray> TaskConnectionsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskConnectionsRoles::TaskConnectionsRole] = "connectionData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -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 <QAbstractListModel>
#include <QObject>
#include <Flow.hpp>
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<int, QByteArray> roleNames() const override;
private:
Flow *m_flow;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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

View File

@@ -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 <QQuickItem>
#include "TaskPortModel.hpp"
#include <BaseTask.hpp>
#include <TaskPort.hpp>
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

View File

@@ -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<int, QByteArray> TaskModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskRoles::TaskIdRole] = "taskId";
roles[TaskRoles::TaskDataRole] = "taskData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -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 <QAbstractListModel>
#include <Flow.hpp>
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<int, QByteArray> roleNames() const override;
private:
Flow *m_flow;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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

View File

@@ -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 <TaskPort.hpp>
#include <QQuickItem>
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

View File

@@ -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<TaskPort *> &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<int, QByteArray> TaskPortModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskPortRoles::TaskPortNameRole] = "taskPortName";
roles[TaskPortRoles::TaskPortDataRole] = "taskPortData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -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 <QAbstractListModel>
#include <BaseTask.hpp>
namespace QodeAssist::TaskFlow {
class TaskPortModel : public QAbstractListModel
{
Q_OBJECT
public:
enum TaskPortRoles { TaskPortNameRole = Qt::UserRole, TaskPortDataRole };
TaskPortModel(const QList<TaskPort *> &ports, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
QList<TaskPort *> m_ports;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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
// }
}

View File

@@ -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)
}
}
}

View File

@@ -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
// }
// }
// }
}
}

View File

@@ -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()
}

View File

@@ -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 {
}

View File

@@ -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 }
}
}

View File

@@ -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 <QUuid>
#include <QtConcurrent>
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<TaskPort *> BaseTask::getInputPorts() const
{
QMutexLocker locker(&m_tasksMutex);
return m_inputs;
}
QList<TaskPort *> BaseTask::getOutputPorts() const
{
QMutexLocker locker(&m_tasksMutex);
return m_outputs;
}
QFuture<TaskState> 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

View File

@@ -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 <QFuture>
#include <QMetaType>
#include <QMutex>
#include <QObject>
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<TaskPort *> getInputPorts() const;
QList<TaskPort *> getOutputPorts() const;
virtual TaskState execute() = 0;
static QString taskStateAsString(TaskState state);
protected:
QFuture<TaskState> executeAsync();
private:
QString m_taskId;
QList<TaskPort *> m_inputs;
QList<TaskPort *> m_outputs;
mutable QMutex m_tasksMutex;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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}
)

View File

@@ -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 <QUuid>
#include <QtConcurrent>
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<QString, BaseTask *> 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<BaseTask *>(sourcePort->parent());
BaseTask *targetTask = qobject_cast<BaseTask *>(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<TaskConnection *> Flow::connections() const
{
QMutexLocker locker(&m_flowMutex);
return m_connections;
}
QFuture<FlowState> 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<BaseTask *> 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<BaseTask *> Flow::getExecutionOrder() const
{
QMutexLocker locker(&m_flowMutex);
QList<BaseTask *> result;
QSet<BaseTask *> visited;
QList<BaseTask *> allTasks = m_tasks.values();
std::function<void(BaseTask *)> visit = [&](BaseTask *task) {
if (visited.contains(task)) {
return;
}
visited.insert(task);
QList<BaseTask *> 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<BaseTask *> visited;
QSet<BaseTask *> 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<BaseTask *> &visited, QSet<BaseTask *> &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<BaseTask *> Flow::getTaskDependencies(BaseTask *task) const
{
QList<BaseTask *> 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

View File

@@ -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 <QFuture>
#include <QHash>
#include <QList>
#include <QMetaType>
#include <QMutex>
#include <QObject>
#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<QString, BaseTask *> tasks() const;
TaskConnection *addConnection(TaskPort *sourcePort, TaskPort *targetPort);
void removeConnection(TaskConnection *connection);
QList<TaskConnection *> connections() const;
QFuture<FlowState> 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<QString, BaseTask *> m_tasks;
QList<TaskConnection *> m_connections;
mutable QMutex m_flowMutex;
QList<BaseTask *> getExecutionOrder() const;
bool detectCircularDependencies() const;
void visitTask(
BaseTask *task,
QSet<BaseTask *> &visited,
QSet<BaseTask *> &recursionStack,
bool &hasCycle) const;
QList<BaseTask *> getTaskDependencies(BaseTask *task) const;
};
} // namespace QodeAssist::TaskFlow
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::Flow *)
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::FlowState)

View File

@@ -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 <Logger.hpp>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#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<QString, Flow *> 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

View File

@@ -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 <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QString>
#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<QString, Flow *> 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<QString, Flow *> m_flows;
TaskRegistry *m_taskRegistry;
FlowRegistry *m_flowRegistry;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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

View File

@@ -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 <functional>
#include <QHash>
#include <QObject>
#include <QString>
namespace QodeAssist::TaskFlow {
class Flow;
class FlowManager;
class FlowRegistry : public QObject
{
Q_OBJECT
public:
using FlowCreator = std::function<Flow *(FlowManager *flowManager)>;
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<QString, FlowCreator> m_flowCreators;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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 <QMetaEnum>
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<BaseTask *>(m_sourcePort->parent()) : nullptr;
}
BaseTask *TaskConnection::targetTask() const
{
return m_targetPort ? qobject_cast<BaseTask *>(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<TaskPort::ValueType>();
qWarning() << "TaskConnection::setupConnection - Type incompatible connection:"
<< metaEnum.valueToKey(static_cast<int>(m_sourcePort->valueType())) << "to"
<< metaEnum.valueToKey(static_cast<int>(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

View File

@@ -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 <QObject>
#include <QString>
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

View File

@@ -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 <QMetaEnum>
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<ValueType>().valueToKey(static_cast<int>(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<QString>();
case ValueType::Number:
return value.canConvert<double>() || value.canConvert<int>();
case ValueType::Boolean:
return value.canConvert<bool>();
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

View File

@@ -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 <QObject>
#include <QString>
#include <QVariant>
#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)

View File

@@ -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 <Logger.hpp>
#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

View File

@@ -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 <functional>
#include <QHash>
#include <QObject>
#include <QString>
namespace QodeAssist::TaskFlow {
class BaseTask;
class TaskRegistry : public QObject
{
Q_OBJECT
public:
using TaskCreator = std::function<BaseTask *(QObject *parent)>;
explicit TaskRegistry(QObject *parent = nullptr);
template<typename T>
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<QString, TaskCreator> m_creators;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -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 <AgentFactory.hpp>
#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<Mcp::McpServerManager> m_mcpServerManager;
QPointer<QQmlEngine> m_engine;
QPointer<Skills::SkillsManager> m_skillsManager;
#ifdef QODEASSIST_EXPERIMENTAL
QPointer<Providers::ProviderInstanceFactory> m_providerInstanceFactory;
QPointer<Providers::ProviderSecretsStore> m_providerSecretsStore;
QPointer<Providers::ProviderLauncher> m_providerLauncher;
QPointer<Settings::ProvidersPageNavigator> m_providersPageNavigator;
std::unique_ptr<Core::IOptionsPage> m_providersOptionsPage;
QPointer<AgentFactory> m_agentFactory;
QPointer<Settings::AgentsPageNavigator> m_agentsPageNavigator;
std::unique_ptr<Core::IOptionsPage> m_agentsOptionsPage;
QPointer<Settings::AgentPipelinesPageNavigator> m_agentPipelinesPageNavigator;
std::unique_ptr<Core::IOptionsPage> m_agentPipelinesOptionsPage;
#endif
};
} // namespace QodeAssist::Internal

View File

@@ -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 <ProviderInstance.hpp>
#include <ProviderInstanceFactory.hpp>
#include <QColor>
#include <QComboBox>
#include <QEvent>
#include <QFile>
#include <QFont>
#include <QFrame>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QScopedValueRollback>
#include <QToolButton>
#include <QVBoxLayout>
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<bool> 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

View File

@@ -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 <QPointer>
#include <QStringList>
#include <QWidget>
#include <AgentConfig.hpp>
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<Providers::ProviderInstanceFactory> 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

View File

@@ -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 <Agent.hpp>
#include <AgentConfig.hpp>
#include <AgentFactory.hpp>
#include <QByteArray>
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSaveFile>
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

View File

@@ -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 <QString>
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

View File

@@ -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 <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPalette>
#include <QScopedValueRollback>
#include <QVBoxLayout>
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<QString> &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<bool> 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

View File

@@ -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 <QFrame>
#include <QList>
#include <QSet>
#include <QString>
#include <AgentConfig.hpp>
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<QString> &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<TagChip *> m_chips;
};
} // namespace QodeAssist::Settings

View File

@@ -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 <Agent.hpp>
#include <AgentFactory.hpp>
#include <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMap>
#include <QPalette>
#include <QScrollArea>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
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<QString> &) { 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<QString, int> 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<const AgentConfig *> AgentListPane::visibleAgents() const
{
std::vector<const AgentConfig *> 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<QString> &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<const AgentConfig *> userAgents;
std::vector<const AgentConfig *> 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<AgentListItem *> newRows;
auto *content = new QWidget;
content->setAutoFillBackground(true);
auto *contentLayout = new QVBoxLayout(content);
contentLayout->setContentsMargins(0, 0, 0, 0);
contentLayout->setSpacing(0);
const QSet<QString> &activeTags = m_tagStrip->activeTags();
auto addAgents = [&](const std::vector<const AgentConfig *> &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

View File

@@ -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 <QFrame>
#include <QList>
#include <QSet>
#include <QString>
#include <vector>
#include <AgentConfig.hpp>
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<const AgentConfig *> 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<AgentListItem *> m_rows;
QString m_currentName;
};
} // namespace QodeAssist::Settings

View File

@@ -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 <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/editormanager/editormanager.h>
#include <utils/filepath.h>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFont>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPalette>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
#include <QSplitter>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
#include <Agent.hpp>
#include <AgentFactory.hpp>
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<AgentsPageNavigator> 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<Core::IOptionsPage> createAgentsSettingsPage(
AgentFactory *agentFactory, AgentsPageNavigator *navigator)
{
return std::make_unique<AgentsSettingsPage>(agentFactory, navigator);
}
} // namespace QodeAssist::Settings
#include "AgentsSettingsPage.moc"

View File

@@ -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 <memory>
#include <QObject>
#include <QString>
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<Core::IOptionsPage> createAgentsSettingsPage(
AgentFactory *agentFactory, AgentsPageNavigator *navigator);
} // namespace QodeAssist::Settings

View File

@@ -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})

View File

@@ -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 <array>
#include <QFrame>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QToolButton>
#include <QVBoxLayout>
#include <solutions/terminal/terminalview.h>
#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/<agent endpoint>").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(" <span style='color:gray'>(detached — survives Qt Creator restart)</span>")
: QString();
m_launchCmdLabel->setText(
QStringLiteral("<b>%1</b> %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<QColor, 16> ansi = dark
? std::array<QColor, 16>{
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, 16>{
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<QColor, 20> 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

View File

@@ -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 <QWidget>
#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

View File

@@ -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 <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QScopedValueRollback>
#include <QVBoxLayout>
#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<bool> 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

View File

@@ -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 <QFrame>
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

View File

@@ -256,8 +256,6 @@ public:
}
};
#ifndef QODEASSIST_EXPERIMENTAL
const ProviderSettingsPage providerSettingsPage;
#endif
} // namespace QodeAssist::Settings

View File

@@ -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 <algorithm>
#include <vector>
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <utils/filepath.h>
#include <QDialog>
#include <QFile>
#include <QFileInfo>
#include <QFrame>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
#include <QSplitter>
#include <QTimer>
#include <QVBoxLayout>
#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<const Providers::ProviderInstance *> 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<Providers::ProviderInstanceFactory> m_factory;
QPointer<Providers::ProviderSecretsStore> m_secrets;
QPointer<ProvidersPageNavigator> m_navigator;
QLabel *m_titleLabel = nullptr;
QLineEdit *m_filterEdit = nullptr;
QScrollArea *m_listScroll = nullptr;
QWidget *m_listContent = nullptr;
QVBoxLayout *m_listLayout = nullptr;
QList<ProviderListItem *> m_rows;
QScrollArea *m_detailScroll = nullptr;
ProviderDetailPane *m_detailPane = nullptr;
QString m_currentName;
QPointer<Providers::ProviderLauncher> 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<Core::IOptionsPage> createProvidersSettingsPage(
Providers::ProviderInstanceFactory *instanceFactory,
Providers::ProviderSecretsStore *secrets,
Providers::ProviderLauncher *launcher,
ProvidersPageNavigator *navigator)
{
return std::make_unique<ProvidersOptionsPage>(
instanceFactory, secrets, launcher, navigator);
}
} // namespace QodeAssist::Settings
#include "ProvidersSettingsPage.moc"

View File

@@ -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 <memory>
#include <QObject>
#include <QString>
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<Core::IOptionsPage> createProvidersSettingsPage(
Providers::ProviderInstanceFactory *instanceFactory,
Providers::ProviderSecretsStore *secrets,
Providers::ProviderLauncher *launcher,
ProvidersPageNavigator *navigator);
} // namespace QodeAssist::Settings

View File

@@ -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 <QLabel>
#include <QVBoxLayout>
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

View File

@@ -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 <QWidget>
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

View File

@@ -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";

View File

@@ -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 <QFont>
#include <QFontDatabase>
#include <QPalette>
#include <QString>
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

View File

@@ -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 <QFont>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPalette>
#include <QWidget>
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

View File

@@ -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 <QString>
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

View File

@@ -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 <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPalette>
#include <QScopedValueRollback>
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<bool> 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

View File

@@ -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 <QFrame>
#include <QString>
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

View File

@@ -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 <QEvent>
#include <QFont>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLayoutItem>
#include <QPalette>
#include <QScopedValueRollback>
#include <QStringList>
#include <QTimer>
#include <QVBoxLayout>
#include <algorithm>
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<QString, int> &countsByTag)
{
m_counts = countsByTag;
QSet<QString> stillExisting;
for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it)
stillExisting.insert(it.key());
QSet<QString> 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<bool> 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("<a href=\"#\">%1</a>").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<std::pair<QString, int>> 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

View File

@@ -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 <QHash>
#include <QMap>
#include <QSet>
#include <QString>
#include <QWidget>
class QVBoxLayout;
namespace QodeAssist::Settings {
class TagChip;
class TagFilterStrip : public QWidget
{
Q_OBJECT
public:
explicit TagFilterStrip(QWidget *parent = nullptr);
void setAvailableTags(const QMap<QString, int> &countsByTag);
const QSet<QString> &activeTags() const { return m_activeTags; }
signals:
void activeTagsChanged(const QSet<QString> &tags);
protected:
void changeEvent(QEvent *event) override;
private:
void rebuild();
void refreshActiveStates();
void applyTheme();
void toggleTag(const QString &tag);
QMap<QString, int> m_counts;
QSet<QString> m_activeTags;
QVBoxLayout *m_layout = nullptr;
QHash<QString, TagChip *> m_chipByTag;
bool m_inApplyTheme = false;
};
} // namespace QodeAssist::Settings

View File

@@ -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()

View File

@@ -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 <QThread>
#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<QList<QString>> 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<QString>{});
}
return m_provider->getInstalledModels(m_provider->url());
}
} // namespace QodeAssist

View File

@@ -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 <memory>
#include <QFuture>
#include <QList>
#include <QObject>
#include <QString>
#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<QList<QString>> installedModels();
private:
AgentConfig m_config;
std::unique_ptr<Templates::JsonPromptTemplate> m_promptTemplate; // owned
Providers::Provider *m_provider = nullptr; // child of this
QString m_invalidReason;
};
} // namespace QodeAssist

View File

@@ -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 <QJsonObject>
#include <QString>
#include <QStringList>
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

View File

@@ -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 <QLoggingCategory>
#include <QThread>
#include <coreplugin/icore.h>
#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<qsizetype>(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<qsizetype>(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<Agent>(*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<Agent>(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

View File

@@ -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 <vector>
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QStringList>
#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<AgentConfig> &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<AgentConfig> m_configs;
QHash<QString, qsizetype> m_indexByName;
QStringList m_errors;
QStringList m_warnings;
QPointer<Providers::ProviderInstanceFactory> m_instanceFactory;
QPointer<Providers::ProviderSecretsStore> m_secrets;
};
} // namespace QodeAssist

View File

@@ -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 <QDir>
#include <QFile>
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSet>
#include <toml++/toml.hpp>
#include <algorithm>
#include <sstream>
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<qint64>(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<QJsonObject> 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<QString, RawEntry> &raw,
QSet<QString> &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<AgentConfig> 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<QString, RawEntry> 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<QString> 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

View File

@@ -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 <QString>
#include <QStringList>
#include <vector>
#include "AgentConfig.hpp"
namespace QodeAssist::Agents {
class AgentLoader
{
public:
struct LoadResult
{
std::vector<AgentConfig> configs;
QStringList errors;
QStringList warnings;
};
static LoadResult load(const QString &qrcPrefix, const QString &userDir);
static std::optional<AgentConfig> parseFile(
const QString &path, QString *error, QStringList *warnings = nullptr);
};
} // namespace QodeAssist::Agents

View File

@@ -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 <QFileInfo>
#include <QRegularExpression>
#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

View File

@@ -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 <QString>
#include <QStringList>
#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

View File

@@ -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}
)

View File

@@ -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 <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStringList>
#include <filesystem>
#include <inja/inja.hpp>
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<std::string>();
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<std::string>()), 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<std::string>()), 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<std::string>());
const int n = args.at(1)->get<int>();
if (n <= 0)
return std::string{};
const QStringList lines = text.split('\n');
const int take = std::min<int>(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<std::string>()))
.fileName()
.toStdString();
});
env.add_callback("dirname", 1, [](inja::Arguments &args) -> nlohmann::json {
return QFileInfo(QString::fromStdString(args.at(0)->get<std::string>()))
.path()
.toStdString();
});
env.add_callback("ext", 1, [](inja::Arguments &args) -> nlohmann::json {
return QFileInfo(QString::fromStdString(args.at(0)->get<std::string>()))
.suffix()
.toStdString();
});
env.add_callback("lower", 1, [](inja::Arguments &args) -> nlohmann::json {
return QString::fromStdString(args.at(0)->get<std::string>()).toLower().toStdString();
});
env.add_callback("upper", 1, [](inja::Arguments &args) -> nlohmann::json {
return QString::fromStdString(args.at(0)->get<std::string>()).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

View File

@@ -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 <QString>
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

View File

@@ -1,9 +0,0 @@
<RCC>
<qresource prefix="/agents">
<file>ollama_base_chat.toml</file>
<file>ollama_base_fim.toml</file>
<file>ollama_gemma4_e4b_chat.toml</file>
<file>ollama_codellama_7b_code_fim.toml</file>
<file>ollama_codellama_13b_qml_fim.toml</file>
</qresource>
</RCC>

View File

@@ -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"

View File

@@ -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 = ["<EOT>"]

View File

@@ -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("<SUF>" + ctx.suffix + "<PRE>" + ctx.prefix + "<MID>") }}
{%- else -%}
{{ tojson("<PRE>" + ctx.prefix + "<MID>") }}
{%- 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 = ["<SUF>", "<PRE>", "</PRE>", "</SUF>", "< EOT >", "\\end", "<MID>", "</MID>", "##"]

View File

@@ -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("<PRE> " + ctx.prefix + " <SUF>" + ctx.suffix + " <MID>") }}
{%- 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 = ["<EOT>", "<PRE>", "<SUF>", "<MID>"]

View File

@@ -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

View File

@@ -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
)

View File

@@ -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 <QJsonObject>
#include <QString>
#include <QVector>
#include <optional>
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<ContentBlockEntry> 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<QString> systemPrompt = std::nullopt;
std::optional<QString> prefix = std::nullopt;
std::optional<QString> suffix = std::nullopt;
std::optional<QString> fileContext = std::nullopt;
std::optional<QVector<Message>> history = std::nullopt;
std::optional<QList<FileMetadata>> filesMetadata = std::nullopt;
bool operator==(const ContextData &) const = default;
};
} // namespace QodeAssist::Templates

View File

@@ -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
)

View File

@@ -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 <LLMQore/BaseClient.hpp>
#include <LLMQore/ToolsManager.hpp>
#include <QJsonArray>
#include <QJsonDocument>
#include <Logger.hpp>
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

View File

@@ -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 <QFlags>
#include <QFuture>
#include <QObject>
#include <QString>
#include <utils/environment.h>
#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<QList<QString>> 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

View File

@@ -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 <QHash>
namespace QodeAssist::Providers::ProviderFactory {
namespace {
QHash<QString, FactoryFn> &table()
{
static QHash<QString, FactoryFn> 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

View File

@@ -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 <QObject>
#include <QString>
#include <functional>
namespace QodeAssist::Providers {
class Provider;
namespace ProviderFactory {
using FactoryFn = std::function<Provider *(QObject *parent)>;
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

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More