mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-18 17:20:58 -04:00
refactor: Remove all experimental code
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -256,8 +256,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
#ifndef QODEASSIST_EXPERIMENTAL
|
||||
const ProviderSettingsPage providerSettingsPage;
|
||||
#endif
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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>"]
|
||||
@@ -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>", "##"]
|
||||
@@ -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>"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user