From 01360b961d8bb6ea1ecd34f5e69781e5a730b2fc Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 8 Oct 2026 12:14:02 +0200 Subject: [PATCH] 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 --- kde/gui/ChatPane.qml | 10 ++ kde/gui/ProcManagerDialog.qml | 205 ++++++++++++++++++++++++++++++++++ kde/gui/main.qml | 5 + kde/gui/ollie9pclient.cpp | 69 ++++++++++++ kde/gui/ollie9pclient.h | 5 + kde/gui/qml.qrc | 1 + 6 files changed, 295 insertions(+) create mode 100644 kde/gui/ProcManagerDialog.qml diff --git a/kde/gui/ChatPane.qml b/kde/gui/ChatPane.qml index b23e382..7bc8ee5 100644 --- a/kde/gui/ChatPane.qml +++ b/kde/gui/ChatPane.qml @@ -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 } diff --git a/kde/gui/ProcManagerDialog.qml b/kde/gui/ProcManagerDialog.qml new file mode 100644 index 0000000..d85a337 --- /dev/null +++ b/kde/gui/ProcManagerDialog.qml @@ -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 + } +} diff --git a/kde/gui/main.qml b/kde/gui/main.qml index 2673450..786b614 100644 --- a/kde/gui/main.qml +++ b/kde/gui/main.qml @@ -189,6 +189,11 @@ ApplicationWindow { id: goalDialog } + // --- Process Manager Dialog --- + ProcManagerDialog { + id: procManagerDialog + } + SplitView { anchors.fill: parent diff --git a/kde/gui/ollie9pclient.cpp b/kde/gui/ollie9pclient.cpp index 710a904..a72cb65 100644 --- a/kde/gui/ollie9pclient.cpp +++ b/kde/gui/ollie9pclient.cpp @@ -231,6 +231,13 @@ void Ollie9pClient::handleEvent(const QString &eventLine) } } else if (agentAction == QLatin1String("new") || agentAction == QLatin1String("kill")) { 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."))) { const QStringView bypassAction = agentAction.mid(7); // after "bypass." @@ -895,6 +902,68 @@ bool Ollie9pClient::resolveBypass(const QString &sessionId, const QString &agent 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: pidstatusexit_codetoolcmdstart_unixruntime_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) --- void Ollie9pClient::ensureRootDataLoaded() diff --git a/kde/gui/ollie9pclient.h b/kde/gui/ollie9pclient.h index 144f59e..89780e5 100644 --- a/kde/gui/ollie9pclient.h +++ b/kde/gui/ollie9pclient.h @@ -96,6 +96,10 @@ public: 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 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(); } signals: @@ -109,6 +113,7 @@ signals: 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 pendingBypassCountChanged(const QString &sessionId, const QString &agentId); + void procStarted(const QString &sessionId, const QString &agentId, const QString &payload); void rootBackendsLoadedChanged(); void availableBackendsChanged(); void rootAgentsLoadedChanged(); diff --git a/kde/gui/qml.qrc b/kde/gui/qml.qrc index 1d55f6e..3a80b87 100644 --- a/kde/gui/qml.qrc +++ b/kde/gui/qml.qrc @@ -5,6 +5,7 @@ NewSessionDialog.qml NewAgentDialog.qml GoalDialog.qml + ProcManagerDialog.qml ChatBlockDelegate.qml SessionTree.qml ChatPane.qml