gui: add Peers button and peer management window
Canvas-based graph visualization of agents in current session: - Nodes represent agents, colored by state - Edges represent peer connections - Drag nodes to reposition - Click two nodes to toggle connection - Double-click to start connecting - Right-click for context menu - Pan with middle mouse, zoom with scroll wheel - Auto-layout arranges nodes in circle 9P API methods: listPeers, addPeer, removePeer, listAgentsInSession
This commit is contained in:
parent
05dd33c1cf
commit
56f6d3c655
|
|
@ -400,6 +400,16 @@ Item {
|
||||||
ToolTip.text: "Background processes"
|
ToolTip.text: "Background processes"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Peers button
|
||||||
|
ToolButton {
|
||||||
|
text: "🔗 Peers"
|
||||||
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
||||||
|
implicitHeight: 28
|
||||||
|
onClicked: peerManagerWindow.show()
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Manage peer connections"
|
||||||
|
}
|
||||||
|
|
||||||
Item { Layout.fillWidth: true } // Spacer
|
Item { Layout.fillWidth: true } // Spacer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Window
|
||||||
|
|
||||||
|
Window {
|
||||||
|
id: peerWindow
|
||||||
|
title: "Peer Connections" + (currentSessionName ? " — " + currentSessionName : "")
|
||||||
|
width: 700
|
||||||
|
height: 500
|
||||||
|
minimumWidth: 500
|
||||||
|
minimumHeight: 400
|
||||||
|
|
||||||
|
property string currentSessionId: ollie.activeSessionId
|
||||||
|
property string currentAgentId: ollie.activeAgentId
|
||||||
|
property string currentSessionName: ""
|
||||||
|
property var agents: [] // [{id, name, x, y, peers: [name1, name2...]}]
|
||||||
|
property var selectedNode: null
|
||||||
|
property var connectingFrom: null // Node we're drawing connection from
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!currentSessionId) {
|
||||||
|
agents = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var agentList = ollie.listAgentsInSession(currentSessionId)
|
||||||
|
var newAgents = []
|
||||||
|
var radius = Math.min(canvas.width, canvas.height) * 0.35
|
||||||
|
var centerX = canvas.width / 2
|
||||||
|
var centerY = canvas.height / 2
|
||||||
|
|
||||||
|
for (var i = 0; i < agentList.length; i++) {
|
||||||
|
var a = agentList[i]
|
||||||
|
// Skip sub-agents (depth > 0)
|
||||||
|
if (a.depth > 0) continue
|
||||||
|
|
||||||
|
var peers = ollie.listPeers(currentSessionId, a.id)
|
||||||
|
|
||||||
|
// Position in circle, or keep existing position
|
||||||
|
var existing = findAgent(a.id)
|
||||||
|
var angle = (2 * Math.PI * newAgents.length) / Math.max(agentList.length, 1) - Math.PI / 2
|
||||||
|
|
||||||
|
newAgents.push({
|
||||||
|
id: a.id,
|
||||||
|
name: a.name,
|
||||||
|
state: a.state,
|
||||||
|
x: existing ? existing.x : centerX + radius * Math.cos(angle),
|
||||||
|
y: existing ? existing.y : centerY + radius * Math.sin(angle),
|
||||||
|
peers: peers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
agents = newAgents
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAgent(id) {
|
||||||
|
for (var i = 0; i < agents.length; i++) {
|
||||||
|
if (agents[i].id === id) return agents[i]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAgentByName(name) {
|
||||||
|
for (var i = 0; i < agents.length; i++) {
|
||||||
|
if (agents[i].name === name) return agents[i]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeAt(x, y) {
|
||||||
|
var nodeRadius = 30
|
||||||
|
for (var i = agents.length - 1; i >= 0; i--) {
|
||||||
|
var a = agents[i]
|
||||||
|
var dx = x - a.x
|
||||||
|
var dy = y - a.y
|
||||||
|
if (dx * dx + dy * dy <= nodeRadius * nodeRadius) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSessionName() {
|
||||||
|
if (currentSessionId) {
|
||||||
|
var cfg = ollie.readSessionConfig(currentSessionId)
|
||||||
|
currentSessionName = cfg.name || currentSessionId
|
||||||
|
} else {
|
||||||
|
currentSessionName = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: refreshTimer
|
||||||
|
interval: 3000
|
||||||
|
running: peerWindow.visible
|
||||||
|
repeat: true
|
||||||
|
onTriggered: refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
currentSessionId = ollie.activeSessionId
|
||||||
|
currentAgentId = ollie.activeAgentId
|
||||||
|
updateSessionName()
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ollie
|
||||||
|
function onActiveSessionIdChanged() {
|
||||||
|
if (peerWindow.visible) {
|
||||||
|
currentSessionId = ollie.activeSessionId
|
||||||
|
currentAgentId = ollie.activeAgentId
|
||||||
|
updateSessionName()
|
||||||
|
selectedNode = null
|
||||||
|
connectingFrom = null
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "Drag nodes to position. Click two nodes to connect/disconnect. Right-click for options."
|
||||||
|
opacity: 0.7
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas for graph
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Canvas {
|
||||||
|
id: canvas
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
property real panX: 0
|
||||||
|
property real panY: 0
|
||||||
|
property real zoom: 1.0
|
||||||
|
property point lastMouse
|
||||||
|
property bool panning: false
|
||||||
|
property var draggingNode: null
|
||||||
|
property point dragOffset
|
||||||
|
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d")
|
||||||
|
ctx.reset()
|
||||||
|
ctx.fillStyle = palette.base
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(panX, panY)
|
||||||
|
ctx.scale(zoom, zoom)
|
||||||
|
|
||||||
|
var nodeRadius = 30
|
||||||
|
|
||||||
|
// Draw edges first
|
||||||
|
ctx.strokeStyle = palette.highlight
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
for (var i = 0; i < agents.length; i++) {
|
||||||
|
var a = agents[i]
|
||||||
|
for (var j = 0; j < a.peers.length; j++) {
|
||||||
|
var peer = findAgentByName(a.peers[j])
|
||||||
|
if (peer && peer.id > a.id) { // Draw each edge once
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(a.x, a.y)
|
||||||
|
ctx.lineTo(peer.x, peer.y)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw connection preview
|
||||||
|
if (connectingFrom && mouseArea.containsMouse) {
|
||||||
|
ctx.strokeStyle = "#888"
|
||||||
|
ctx.setLineDash([5, 5])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(connectingFrom.x, connectingFrom.y)
|
||||||
|
var mx = (mouseArea.mouseX - panX) / zoom
|
||||||
|
var my = (mouseArea.mouseY - panY) / zoom
|
||||||
|
ctx.lineTo(mx, my)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.setLineDash([])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
for (var i = 0; i < agents.length; i++) {
|
||||||
|
var a = agents[i]
|
||||||
|
var isSelected = selectedNode && selectedNode.id === a.id
|
||||||
|
var isConnecting = connectingFrom && connectingFrom.id === a.id
|
||||||
|
var isActive = a.id === currentAgentId
|
||||||
|
|
||||||
|
// Node circle
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(a.x, a.y, nodeRadius, 0, 2 * Math.PI)
|
||||||
|
|
||||||
|
// Fill based on state
|
||||||
|
if (a.state === "idle") {
|
||||||
|
ctx.fillStyle = "#4CAF50"
|
||||||
|
} else if (a.state === "thinking" || a.state === "calling") {
|
||||||
|
ctx.fillStyle = "#2196F3"
|
||||||
|
} else if (a.state === "paused") {
|
||||||
|
ctx.fillStyle = "#9E9E9E"
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = "#FF9800"
|
||||||
|
}
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Border
|
||||||
|
if (isSelected || isConnecting) {
|
||||||
|
ctx.strokeStyle = palette.highlightedText
|
||||||
|
ctx.lineWidth = 3
|
||||||
|
} else if (isActive) {
|
||||||
|
ctx.strokeStyle = palette.highlight
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
} else {
|
||||||
|
ctx.strokeStyle = palette.text
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
}
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Label
|
||||||
|
ctx.fillStyle = palette.text
|
||||||
|
ctx.font = "12px sans-serif"
|
||||||
|
ctx.textAlign = "center"
|
||||||
|
ctx.textBaseline = "middle"
|
||||||
|
var label = a.name.length > 10 ? a.name.substring(0, 9) + "…" : a.name
|
||||||
|
ctx.fillText(label, a.x, a.y + nodeRadius + 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
|
||||||
|
onPressed: function(mouse) {
|
||||||
|
var x = (mouse.x - canvas.panX) / canvas.zoom
|
||||||
|
var y = (mouse.y - canvas.panY) / canvas.zoom
|
||||||
|
var node = nodeAt(x, y)
|
||||||
|
|
||||||
|
if (mouse.button === Qt.MiddleButton) {
|
||||||
|
canvas.panning = true
|
||||||
|
canvas.lastMouse = Qt.point(mouse.x, mouse.y)
|
||||||
|
} else if (mouse.button === Qt.RightButton && node) {
|
||||||
|
selectedNode = node
|
||||||
|
nodeContextMenu.popup()
|
||||||
|
} else if (mouse.button === Qt.LeftButton) {
|
||||||
|
if (node) {
|
||||||
|
if (connectingFrom) {
|
||||||
|
// Complete connection
|
||||||
|
if (connectingFrom.id !== node.id) {
|
||||||
|
toggleConnection(connectingFrom, node)
|
||||||
|
}
|
||||||
|
connectingFrom = null
|
||||||
|
} else {
|
||||||
|
// Start drag or connection
|
||||||
|
canvas.draggingNode = node
|
||||||
|
canvas.dragOffset = Qt.point(x - node.x, y - node.y)
|
||||||
|
selectedNode = node
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Click on empty space
|
||||||
|
if (connectingFrom) {
|
||||||
|
connectingFrom = null
|
||||||
|
} else {
|
||||||
|
selectedNode = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onReleased: function(mouse) {
|
||||||
|
if (canvas.panning) {
|
||||||
|
canvas.panning = false
|
||||||
|
}
|
||||||
|
if (canvas.draggingNode) {
|
||||||
|
canvas.draggingNode = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: function(mouse) {
|
||||||
|
if (canvas.panning) {
|
||||||
|
canvas.panX += mouse.x - canvas.lastMouse.x
|
||||||
|
canvas.panY += mouse.y - canvas.lastMouse.y
|
||||||
|
canvas.lastMouse = Qt.point(mouse.x, mouse.y)
|
||||||
|
canvas.requestPaint()
|
||||||
|
} else if (canvas.draggingNode) {
|
||||||
|
var x = (mouse.x - canvas.panX) / canvas.zoom
|
||||||
|
var y = (mouse.y - canvas.panY) / canvas.zoom
|
||||||
|
canvas.draggingNode.x = x - canvas.dragOffset.x
|
||||||
|
canvas.draggingNode.y = y - canvas.dragOffset.y
|
||||||
|
canvas.requestPaint()
|
||||||
|
} else if (connectingFrom) {
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheel: function(wheel) {
|
||||||
|
var factor = wheel.angleDelta.y > 0 ? 1.1 : 0.9
|
||||||
|
var newZoom = Math.max(0.3, Math.min(3.0, canvas.zoom * factor))
|
||||||
|
|
||||||
|
// Zoom toward mouse position
|
||||||
|
var mx = wheel.x
|
||||||
|
var my = wheel.y
|
||||||
|
canvas.panX = mx - (mx - canvas.panX) * (newZoom / canvas.zoom)
|
||||||
|
canvas.panY = my - (my - canvas.panY) * (newZoom / canvas.zoom)
|
||||||
|
canvas.zoom = newZoom
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
onDoubleClicked: function(mouse) {
|
||||||
|
var x = (mouse.x - canvas.panX) / canvas.zoom
|
||||||
|
var y = (mouse.y - canvas.panY) / canvas.zoom
|
||||||
|
var node = nodeAt(x, y)
|
||||||
|
if (node) {
|
||||||
|
// Start connecting from this node
|
||||||
|
connectingFrom = node
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: nodeContextMenu
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Connect to..."
|
||||||
|
enabled: selectedNode !== null
|
||||||
|
onTriggered: {
|
||||||
|
connectingFrom = selectedNode
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Disconnect all"
|
||||||
|
enabled: selectedNode !== null && selectedNode.peers.length > 0
|
||||||
|
onTriggered: {
|
||||||
|
if (selectedNode) {
|
||||||
|
for (var i = 0; i < selectedNode.peers.length; i++) {
|
||||||
|
ollie.removePeer(currentSessionId, selectedNode.id, selectedNode.peers[i])
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuSeparator {}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Switch to agent"
|
||||||
|
enabled: selectedNode !== null
|
||||||
|
onTriggered: {
|
||||||
|
if (selectedNode) {
|
||||||
|
ollie.switchAgent(currentSessionId, selectedNode.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom controls
|
||||||
|
RowLayout {
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "−"
|
||||||
|
implicitWidth: 30
|
||||||
|
implicitHeight: 30
|
||||||
|
onClicked: {
|
||||||
|
canvas.zoom = Math.max(0.3, canvas.zoom * 0.8)
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: Math.round(canvas.zoom * 100) + "%"
|
||||||
|
Layout.preferredWidth: 45
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "+"
|
||||||
|
implicitWidth: 30
|
||||||
|
implicitHeight: 30
|
||||||
|
onClicked: {
|
||||||
|
canvas.zoom = Math.min(3.0, canvas.zoom * 1.25)
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Reset"
|
||||||
|
implicitWidth: 50
|
||||||
|
implicitHeight: 30
|
||||||
|
onClicked: {
|
||||||
|
canvas.zoom = 1.0
|
||||||
|
canvas.panX = 0
|
||||||
|
canvas.panY = 0
|
||||||
|
refresh() // Re-layout nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Refresh"
|
||||||
|
icon.name: "view-refresh"
|
||||||
|
onClicked: refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Auto Layout"
|
||||||
|
icon.name: "view-grid"
|
||||||
|
onClicked: {
|
||||||
|
// Re-position all nodes in a circle
|
||||||
|
var radius = Math.min(canvas.width, canvas.height) * 0.35 / canvas.zoom
|
||||||
|
var centerX = (canvas.width / 2 - canvas.panX) / canvas.zoom
|
||||||
|
var centerY = (canvas.height / 2 - canvas.panY) / canvas.zoom
|
||||||
|
|
||||||
|
for (var i = 0; i < agents.length; i++) {
|
||||||
|
var angle = (2 * Math.PI * i) / agents.length - Math.PI / 2
|
||||||
|
agents[i].x = centerX + radius * Math.cos(angle)
|
||||||
|
agents[i].y = centerY + radius * Math.sin(angle)
|
||||||
|
}
|
||||||
|
canvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Label {
|
||||||
|
visible: connectingFrom !== null
|
||||||
|
text: "Click another node to connect, or empty space to cancel"
|
||||||
|
color: palette.highlight
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Close"
|
||||||
|
icon.name: "window-close"
|
||||||
|
onClicked: peerWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConnection(a, b) {
|
||||||
|
// Check if already connected
|
||||||
|
var connected = false
|
||||||
|
for (var i = 0; i < a.peers.length; i++) {
|
||||||
|
if (a.peers[i] === b.name) {
|
||||||
|
connected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connected) {
|
||||||
|
ollie.removePeer(currentSessionId, a.id, b.name)
|
||||||
|
} else {
|
||||||
|
ollie.addPeer(currentSessionId, a.id, b.name)
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -197,6 +197,12 @@ ApplicationWindow {
|
||||||
visible: false
|
visible: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Peer Manager Window ---
|
||||||
|
PeerManagerWindow {
|
||||||
|
id: peerManagerWindow
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
|
|
||||||
SplitView {
|
SplitView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -971,6 +971,75 @@ void Ollie9pClient::openDirectory(const QString &path)
|
||||||
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
|
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Peer management ---
|
||||||
|
|
||||||
|
QStringList Ollie9pClient::listPeers(const QString &sessionId, const QString &agentId)
|
||||||
|
{
|
||||||
|
QStringList result;
|
||||||
|
if (sessionId.isEmpty() || agentId.isEmpty()) return result;
|
||||||
|
if (!m_9p || !m_9p->isConnected()) return result;
|
||||||
|
|
||||||
|
QString path = "session/" + sessionId + "/agent/" + agentId + "/ctl";
|
||||||
|
QByteArray out = m_9p->rdwr(path, "peers");
|
||||||
|
QString raw = QString::fromUtf8(out).trimmed();
|
||||||
|
|
||||||
|
if (raw.isEmpty() || raw == "(no peers)") return result;
|
||||||
|
|
||||||
|
for (const QString &line : raw.split('\n', Qt::SkipEmptyParts)) {
|
||||||
|
result.append(line.trimmed());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Ollie9pClient::addPeer(const QString &sessionId, const QString &agentId, const QString &peerName)
|
||||||
|
{
|
||||||
|
if (sessionId.isEmpty() || agentId.isEmpty() || peerName.isEmpty()) return false;
|
||||||
|
if (!m_9p || !m_9p->isConnected()) return false;
|
||||||
|
|
||||||
|
QString path = "session/" + sessionId + "/agent/" + agentId + "/ctl";
|
||||||
|
QString data = "peeradd " + peerName;
|
||||||
|
QByteArray out = m_9p->rdwr(path, data.toUtf8());
|
||||||
|
return out.trimmed() == "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Ollie9pClient::removePeer(const QString &sessionId, const QString &agentId, const QString &peerName)
|
||||||
|
{
|
||||||
|
if (sessionId.isEmpty() || agentId.isEmpty() || peerName.isEmpty()) return false;
|
||||||
|
if (!m_9p || !m_9p->isConnected()) return false;
|
||||||
|
|
||||||
|
QString path = "session/" + sessionId + "/agent/" + agentId + "/ctl";
|
||||||
|
QString data = "peerdel " + peerName;
|
||||||
|
QByteArray out = m_9p->rdwr(path, data.toUtf8());
|
||||||
|
return out.trimmed() == "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList Ollie9pClient::listAgentsInSession(const QString &sessionId)
|
||||||
|
{
|
||||||
|
QVariantList result;
|
||||||
|
if (sessionId.isEmpty()) return result;
|
||||||
|
if (!m_9p || !m_9p->isConnected()) return result;
|
||||||
|
|
||||||
|
QString path = "session/" + sessionId + "/agent/idx";
|
||||||
|
QByteArray out = m_9p->read(path);
|
||||||
|
QString raw = QString::fromUtf8(out);
|
||||||
|
|
||||||
|
if (raw.isEmpty()) return result;
|
||||||
|
|
||||||
|
for (const QString &line : raw.split('\n', Qt::SkipEmptyParts)) {
|
||||||
|
QStringList parts = line.split('\t');
|
||||||
|
if (parts.size() < 3) continue;
|
||||||
|
|
||||||
|
QVariantMap agent;
|
||||||
|
agent["id"] = parts.value(1);
|
||||||
|
agent["name"] = parts.value(2);
|
||||||
|
agent["parentAgentId"] = parts.value(3);
|
||||||
|
agent["depth"] = parts.value(4).toInt();
|
||||||
|
agent["state"] = parts.value(5);
|
||||||
|
result.append(agent);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Root data loading (lazy) ---
|
// --- Root data loading (lazy) ---
|
||||||
|
|
||||||
void Ollie9pClient::ensureRootDataLoaded()
|
void Ollie9pClient::ensureRootDataLoaded()
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,12 @@ public:
|
||||||
Q_INVOKABLE void openDirectory(const QString &path);
|
Q_INVOKABLE void openDirectory(const QString &path);
|
||||||
Q_INVOKABLE QString lastError() const { return m_9p ? m_9p->lastError() : QString(); }
|
Q_INVOKABLE QString lastError() const { return m_9p ? m_9p->lastError() : QString(); }
|
||||||
|
|
||||||
|
// Peer management
|
||||||
|
Q_INVOKABLE QStringList listPeers(const QString &sessionId, const QString &agentId);
|
||||||
|
Q_INVOKABLE bool addPeer(const QString &sessionId, const QString &agentId, const QString &peerName);
|
||||||
|
Q_INVOKABLE bool removePeer(const QString &sessionId, const QString &agentId, const QString &peerName);
|
||||||
|
Q_INVOKABLE QVariantList listAgentsInSession(const QString &sessionId);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void sessionsChanged();
|
void sessionsChanged();
|
||||||
void activeSessionIdChanged();
|
void activeSessionIdChanged();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<file>NewAgentDialog.qml</file>
|
<file>NewAgentDialog.qml</file>
|
||||||
<file>GoalDialog.qml</file>
|
<file>GoalDialog.qml</file>
|
||||||
<file>ProcManagerWindow.qml</file>
|
<file>ProcManagerWindow.qml</file>
|
||||||
|
<file>PeerManagerWindow.qml</file>
|
||||||
<file>ChatBlockDelegate.qml</file>
|
<file>ChatBlockDelegate.qml</file>
|
||||||
<file>SessionTree.qml</file>
|
<file>SessionTree.qml</file>
|
||||||
<file>ChatPane.qml</file>
|
<file>ChatPane.qml</file>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue