gui: add process manager dialog
- Add procStarted signal to Ollie9pClient for proc.start events - Add listProcs(), procTerm(), procKill(), procOutput() methods - Create ProcManagerDialog.qml showing background processes for the active session/agent with status, runtime, commands - Add 'Procs' button to action toolbar to open the dialog - Auto-refresh every 2 seconds when dialog is visible - Support terminate, kill, and view output actions
This commit is contained in:
parent
414dae05f8
commit
01360b961d
|
|
@ -356,6 +356,16 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Processes button
|
||||||
|
ToolButton {
|
||||||
|
text: "⚙️ Procs"
|
||||||
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
||||||
|
implicitHeight: 28
|
||||||
|
onClicked: procManagerDialog.open()
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Background processes"
|
||||||
|
}
|
||||||
|
|
||||||
Item { Layout.fillWidth: true } // Spacer
|
Item { Layout.fillWidth: true } // Spacer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: procDialog
|
||||||
|
title: "Background Processes"
|
||||||
|
width: 600
|
||||||
|
height: 400
|
||||||
|
modal: false
|
||||||
|
standardButtons: Dialog.Close
|
||||||
|
|
||||||
|
property var procs: []
|
||||||
|
property string outputText: ""
|
||||||
|
property int selectedPid: -1
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
procs = ollie.listProcs(ollie.activeSessionId, ollie.activeAgentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: refreshTimer
|
||||||
|
interval: 2000
|
||||||
|
running: procDialog.visible
|
||||||
|
repeat: true
|
||||||
|
onTriggered: refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ollie
|
||||||
|
function onProcStarted(sessionId, agentId, payload) {
|
||||||
|
if (sessionId === ollie.activeSessionId && agentId === ollie.activeAgentId) {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
// Process list
|
||||||
|
ListView {
|
||||||
|
id: procList
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
model: procs
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: procList.width
|
||||||
|
height: 48
|
||||||
|
color: selectedPid === modelData.pid ? palette.highlight : (index % 2 ? palette.alternateBase : palette.base)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: selectedPid = modelData.pid
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
// Status indicator
|
||||||
|
Rectangle {
|
||||||
|
width: 12
|
||||||
|
height: 12
|
||||||
|
radius: 6
|
||||||
|
color: modelData.status === "running" ? "green" : (modelData.exitCode === 0 ? "gray" : "red")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PID
|
||||||
|
Label {
|
||||||
|
text: modelData.pid
|
||||||
|
font.family: "monospace"
|
||||||
|
Layout.preferredWidth: 60
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool
|
||||||
|
Label {
|
||||||
|
text: modelData.tool
|
||||||
|
font.weight: Font.Medium
|
||||||
|
Layout.preferredWidth: 80
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command
|
||||||
|
Label {
|
||||||
|
text: modelData.cmd
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime
|
||||||
|
Label {
|
||||||
|
text: formatRuntime(modelData.runtime)
|
||||||
|
font.family: "monospace"
|
||||||
|
Layout.preferredWidth: 60
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit code (if exited)
|
||||||
|
Label {
|
||||||
|
visible: modelData.status !== "running"
|
||||||
|
text: "exit " + modelData.exitCode
|
||||||
|
font.family: "monospace"
|
||||||
|
color: modelData.exitCode === 0 ? "gray" : "red"
|
||||||
|
Layout.preferredWidth: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: procs.length === 0
|
||||||
|
text: "No background processes"
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output pane (collapsible)
|
||||||
|
GroupBox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: outputText ? 150 : 0
|
||||||
|
visible: outputText.length > 0
|
||||||
|
title: "Output (PID " + selectedPid + ")"
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
TextArea {
|
||||||
|
text: outputText
|
||||||
|
readOnly: true
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pointSize: 9
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Refresh"
|
||||||
|
icon.name: "view-refresh"
|
||||||
|
onClicked: refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "View Output"
|
||||||
|
icon.name: "document-preview"
|
||||||
|
enabled: selectedPid > 0
|
||||||
|
onClicked: {
|
||||||
|
outputText = ollie.procOutput(ollie.activeSessionId, ollie.activeAgentId, selectedPid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Terminate"
|
||||||
|
icon.name: "process-stop"
|
||||||
|
enabled: selectedPid > 0 && isRunning(selectedPid)
|
||||||
|
onClicked: {
|
||||||
|
ollie.procTerm(ollie.activeSessionId, ollie.activeAgentId, selectedPid)
|
||||||
|
Qt.callLater(refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Kill"
|
||||||
|
icon.name: "edit-delete"
|
||||||
|
enabled: selectedPid > 0 && isRunning(selectedPid)
|
||||||
|
onClicked: {
|
||||||
|
ollie.procKill(ollie.activeSessionId, ollie.activeAgentId, selectedPid)
|
||||||
|
Qt.callLater(refresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRuntime(seconds) {
|
||||||
|
if (seconds < 60) return seconds.toFixed(1) + "s"
|
||||||
|
if (seconds < 3600) return Math.floor(seconds / 60) + "m " + Math.floor(seconds % 60) + "s"
|
||||||
|
return Math.floor(seconds / 3600) + "h " + Math.floor((seconds % 3600) / 60) + "m"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRunning(pid) {
|
||||||
|
for (var i = 0; i < procs.length; i++) {
|
||||||
|
if (procs[i].pid === pid) {
|
||||||
|
return procs[i].status === "running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -189,6 +189,11 @@ ApplicationWindow {
|
||||||
id: goalDialog
|
id: goalDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Process Manager Dialog ---
|
||||||
|
ProcManagerDialog {
|
||||||
|
id: procManagerDialog
|
||||||
|
}
|
||||||
|
|
||||||
SplitView {
|
SplitView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,13 @@ void Ollie9pClient::handleEvent(const QString &eventLine)
|
||||||
}
|
}
|
||||||
} else if (agentAction == QLatin1String("new") || agentAction == QLatin1String("kill")) {
|
} else if (agentAction == QLatin1String("new") || agentAction == QLatin1String("kill")) {
|
||||||
refreshSessions();
|
refreshSessions();
|
||||||
|
} else if (agentAction.startsWith(QLatin1String("proc."))) {
|
||||||
|
// Process events: proc.start
|
||||||
|
const QStringView procAction = agentAction.mid(5); // after "proc."
|
||||||
|
if (procAction == QLatin1String("start")) {
|
||||||
|
// Process started: payload is "pid\ttool\tcmd"
|
||||||
|
emit procStarted(sessionId.toString(), agentId.toString(), payload.toString());
|
||||||
|
}
|
||||||
} else if (agentAction.startsWith(QLatin1String("bypass."))) {
|
} else if (agentAction.startsWith(QLatin1String("bypass."))) {
|
||||||
const QStringView bypassAction = agentAction.mid(7); // after "bypass."
|
const QStringView bypassAction = agentAction.mid(7); // after "bypass."
|
||||||
|
|
||||||
|
|
@ -895,6 +902,68 @@ bool Ollie9pClient::resolveBypass(const QString &sessionId, const QString &agent
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantList Ollie9pClient::listProcs(const QString &sessionId, const QString &agentId)
|
||||||
|
{
|
||||||
|
QVariantList 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, "proc idx");
|
||||||
|
if (out.isEmpty()) return result;
|
||||||
|
|
||||||
|
// Format: pid<TAB>status<TAB>exit_code<TAB>tool<TAB>cmd<TAB>start_unix<TAB>runtime_sec
|
||||||
|
for (const QString &line : QString::fromUtf8(out).split('\n')) {
|
||||||
|
if (line.isEmpty()) continue;
|
||||||
|
QStringList parts = line.split('\t');
|
||||||
|
if (parts.size() < 7) continue;
|
||||||
|
|
||||||
|
QVariantMap proc;
|
||||||
|
proc["pid"] = parts[0].toInt();
|
||||||
|
proc["status"] = parts[1];
|
||||||
|
proc["exitCode"] = parts[2].toInt();
|
||||||
|
proc["tool"] = parts[3];
|
||||||
|
proc["cmd"] = parts[4];
|
||||||
|
proc["startUnix"] = parts[5].toLongLong();
|
||||||
|
proc["runtime"] = parts[6].toDouble();
|
||||||
|
result.append(proc);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Ollie9pClient::procTerm(const QString &sessionId, const QString &agentId, int pid)
|
||||||
|
{
|
||||||
|
if (sessionId.isEmpty() || agentId.isEmpty()) return false;
|
||||||
|
if (!m_9p || !m_9p->isConnected()) return false;
|
||||||
|
|
||||||
|
QString path = "session/" + sessionId + "/agent/" + agentId + "/ctl";
|
||||||
|
QString data = "proc term " + QString::number(pid);
|
||||||
|
QByteArray out = m_9p->rdwr(path, data.toUtf8());
|
||||||
|
return out.trimmed() == "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Ollie9pClient::procKill(const QString &sessionId, const QString &agentId, int pid)
|
||||||
|
{
|
||||||
|
if (sessionId.isEmpty() || agentId.isEmpty()) return false;
|
||||||
|
if (!m_9p || !m_9p->isConnected()) return false;
|
||||||
|
|
||||||
|
QString path = "session/" + sessionId + "/agent/" + agentId + "/ctl";
|
||||||
|
QString data = "proc kill " + QString::number(pid);
|
||||||
|
QByteArray out = m_9p->rdwr(path, data.toUtf8());
|
||||||
|
return out.trimmed() == "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Ollie9pClient::procOutput(const QString &sessionId, const QString &agentId, int pid)
|
||||||
|
{
|
||||||
|
if (sessionId.isEmpty() || agentId.isEmpty()) return QString();
|
||||||
|
if (!m_9p || !m_9p->isConnected()) return QString();
|
||||||
|
|
||||||
|
QString path = "session/" + sessionId + "/agent/" + agentId + "/ctl";
|
||||||
|
QString data = "proc out " + QString::number(pid);
|
||||||
|
QByteArray out = m_9p->rdwr(path, data.toUtf8());
|
||||||
|
return QString::fromUtf8(out);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Root data loading (lazy) ---
|
// --- Root data loading (lazy) ---
|
||||||
|
|
||||||
void Ollie9pClient::ensureRootDataLoaded()
|
void Ollie9pClient::ensureRootDataLoaded()
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,10 @@ public:
|
||||||
Q_INVOKABLE bool renameSession(const QString &sessionId, const QString &newName);
|
Q_INVOKABLE bool renameSession(const QString &sessionId, const QString &newName);
|
||||||
Q_INVOKABLE bool renameAgent(const QString &sessionId, const QString &agentId, const QString &newName);
|
Q_INVOKABLE bool renameAgent(const QString &sessionId, const QString &agentId, const QString &newName);
|
||||||
Q_INVOKABLE bool resolveBypass(const QString &sessionId, const QString &agentId, const QString &id, bool approve);
|
Q_INVOKABLE bool resolveBypass(const QString &sessionId, const QString &agentId, const QString &id, bool approve);
|
||||||
|
Q_INVOKABLE QVariantList listProcs(const QString &sessionId, const QString &agentId);
|
||||||
|
Q_INVOKABLE bool procTerm(const QString &sessionId, const QString &agentId, int pid);
|
||||||
|
Q_INVOKABLE bool procKill(const QString &sessionId, const QString &agentId, int pid);
|
||||||
|
Q_INVOKABLE QString procOutput(const QString &sessionId, const QString &agentId, int pid);
|
||||||
Q_INVOKABLE QString lastError() const { return m_9p ? m_9p->lastError() : QString(); }
|
Q_INVOKABLE QString lastError() const { return m_9p ? m_9p->lastError() : QString(); }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|
@ -109,6 +113,7 @@ signals:
|
||||||
void bypassRequested(const QString &sessionId, const QString &agentId, const QString &id, const QString &cmd, const QString &cwd);
|
void bypassRequested(const QString &sessionId, const QString &agentId, const QString &id, const QString &cmd, const QString &cwd);
|
||||||
void bypassResolved(const QString &sessionId, const QString &agentId);
|
void bypassResolved(const QString &sessionId, const QString &agentId);
|
||||||
void pendingBypassCountChanged(const QString &sessionId, const QString &agentId);
|
void pendingBypassCountChanged(const QString &sessionId, const QString &agentId);
|
||||||
|
void procStarted(const QString &sessionId, const QString &agentId, const QString &payload);
|
||||||
void rootBackendsLoadedChanged();
|
void rootBackendsLoadedChanged();
|
||||||
void availableBackendsChanged();
|
void availableBackendsChanged();
|
||||||
void rootAgentsLoadedChanged();
|
void rootAgentsLoadedChanged();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<file>NewSessionDialog.qml</file>
|
<file>NewSessionDialog.qml</file>
|
||||||
<file>NewAgentDialog.qml</file>
|
<file>NewAgentDialog.qml</file>
|
||||||
<file>GoalDialog.qml</file>
|
<file>GoalDialog.qml</file>
|
||||||
|
<file>ProcManagerDialog.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