diff --git a/gui/ChatPane.qml b/gui/ChatPane.qml index d392ea8..453d1ab 100644 --- a/gui/ChatPane.qml +++ b/gui/ChatPane.qml @@ -444,6 +444,37 @@ Item { ToolTip.text: "Interrupt" ToolTip.visible: hovered } + + ToolButton { + icon.name: "process-stop" + visible: paneRoot.paneState.startsWith("calling:") + onClicked: dbus.detachProcess(paneRoot.sessionId) + ToolTip.text: "Detach process" + ToolTip.visible: hovered + } + + // Detached process badge + ToolButton { + id: detachBadge + visible: detachBadge.count > 0 + property int count: 0 + text: "⚙ " + count + font.pixelSize: Qt.application.font.pixelSize * 0.85 + onClicked: processManagerWindow.show() + ToolTip.text: count + " detached process" + (count !== 1 ? "es" : "") + ToolTip.visible: hovered + + Timer { + interval: 2000 + running: paneRoot.sessionId !== "" + repeat: true + triggeredOnStart: true + onTriggered: { + let procs = dbus.listDetached(paneRoot.sessionId) + detachBadge.count = procs.length + } + } + } } // Status diff --git a/gui/main-kf5.qml b/gui/main-kf5.qml index 669b652..bdc1082 100644 --- a/gui/main-kf5.qml +++ b/gui/main-kf5.qml @@ -1132,6 +1132,37 @@ ApplicationWindow { ToolTip.text: "Interrupt" ToolTip.visible: hovered } + + ToolButton { + icon.name: "process-stop" + visible: paneRoot.paneState.startsWith("calling:") + onClicked: dbus.detachProcess(paneRoot.sessionId) + ToolTip.text: "Detach process" + ToolTip.visible: hovered + } + + // Detached process badge + ToolButton { + id: detachBadge + visible: detachBadge.count > 0 + property int count: 0 + text: "⚙ " + count + font.pixelSize: Qt.application.font.pixelSize * 0.85 + onClicked: processManagerWindow.show() + ToolTip.text: count + " detached process" + (count !== 1 ? "es" : "") + ToolTip.visible: hovered + + Timer { + interval: 2000 + running: paneRoot.sessionId !== "" + repeat: true + triggeredOnStart: true + onTriggered: { + var procs = dbus.listDetached(paneRoot.sessionId) + detachBadge.count = procs.length + } + } + } } // Status @@ -1712,4 +1743,136 @@ ApplicationWindow { } } } + + // --- Process Manager Window --- + Window { + id: processManagerWindow + title: "Ollie \u2014 Detached Processes" + width: 600 + height: 400 + color: palette.window + + property var processes: [] + property int selectedPid: -1 + property string selectedOutput: "" + + Timer { + id: procRefreshTimer + interval: 1500 + running: processManagerWindow.visible + repeat: true + triggeredOnStart: true + onTriggered: { + if (dbus.activeSessionId === "") return + processManagerWindow.processes = dbus.listDetached(dbus.activeSessionId) + if (processManagerWindow.selectedPid > 0) { + processManagerWindow.selectedOutput = + dbus.getDetachedOutput(dbus.activeSessionId, processManagerWindow.selectedPid) + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + Label { + text: "Session: " + (dbus.activeSessionId || "none") + font.bold: true + } + + ListView { + id: procList + Layout.fillWidth: true + Layout.preferredHeight: 140 + clip: true + model: processManagerWindow.processes + + delegate: ItemDelegate { + width: procList.width + highlighted: modelData.pid === processManagerWindow.selectedPid + + contentItem: RowLayout { + spacing: 8 + + Rectangle { + width: 8; height: 8; radius: 4 + color: modelData.status === "running" ? "#4caf50" : "#9e9e9e" + } + + Label { + text: modelData.command + Layout.fillWidth: true + elide: Text.ElideRight + font.family: "monospace" + } + + Label { + text: "pid " + modelData.pid + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + Label { + text: modelData.status + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + Button { + text: "Term" + visible: modelData.status === "running" + onClicked: dbus.signalDetached(dbus.activeSessionId, modelData.pid, "TERM") + } + + Button { + text: "Kill" + visible: modelData.status === "running" + onClicked: dbus.signalDetached(dbus.activeSessionId, modelData.pid, "KILL") + } + + Button { + text: "Dismiss" + visible: modelData.status !== "running" + onClicked: dbus.dismissDetached(dbus.activeSessionId, modelData.pid) + } + } + + onClicked: { + processManagerWindow.selectedPid = modelData.pid + processManagerWindow.selectedOutput = + dbus.getDetachedOutput(dbus.activeSessionId, modelData.pid) + } + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: palette.mid + } + + Label { + text: processManagerWindow.selectedPid > 0 + ? "\u2014\u2014\u2014 Output: pid " + processManagerWindow.selectedPid + " \u2014\u2014\u2014" + : "Select a process to view output" + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + TextArea { + readOnly: true + font.family: "monospace" + font.pixelSize: Qt.application.font.pixelSize * 0.9 + text: processManagerWindow.selectedOutput + wrapMode: TextEdit.Wrap + } + } + } + } } diff --git a/gui/main.qml b/gui/main.qml index 7369a80..3a5fcb5 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -550,4 +550,139 @@ ApplicationWindow { } } } + + // --- Process Manager Window --- + Window { + id: processManagerWindow + title: "Ollie — Detached Processes" + width: 600 + height: 400 + color: palette.window + + property var processes: [] + property int selectedPid: -1 + property string selectedOutput: "" + + Timer { + id: procRefreshTimer + interval: 1500 + running: processManagerWindow.visible + repeat: true + triggeredOnStart: true + onTriggered: { + if (dbus.activeSessionId === "") return + processManagerWindow.processes = dbus.listDetached(dbus.activeSessionId) + if (processManagerWindow.selectedPid > 0) { + processManagerWindow.selectedOutput = + dbus.getDetachedOutput(dbus.activeSessionId, processManagerWindow.selectedPid) + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + Label { + text: "Session: " + (dbus.activeSessionId || "none") + font.bold: true + } + + // Process list + ListView { + id: procList + Layout.fillWidth: true + Layout.preferredHeight: 140 + clip: true + model: processManagerWindow.processes + + delegate: ItemDelegate { + width: procList.width + highlighted: modelData.pid === processManagerWindow.selectedPid + + contentItem: RowLayout { + spacing: 8 + + Rectangle { + width: 8; height: 8; radius: 4 + color: modelData.status === "running" ? "#4caf50" : "#9e9e9e" + } + + Label { + text: modelData.command + Layout.fillWidth: true + elide: Text.ElideRight + font.family: "monospace" + } + + Label { + text: "pid " + modelData.pid + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + Label { + text: modelData.status + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + Button { + text: "Term" + visible: modelData.status === "running" + onClicked: dbus.signalDetached(dbus.activeSessionId, modelData.pid, "TERM") + } + + Button { + text: "Kill" + visible: modelData.status === "running" + onClicked: dbus.signalDetached(dbus.activeSessionId, modelData.pid, "KILL") + } + + Button { + text: "Dismiss" + visible: modelData.status !== "running" + onClicked: dbus.dismissDetached(dbus.activeSessionId, modelData.pid) + } + } + + onClicked: { + processManagerWindow.selectedPid = modelData.pid + processManagerWindow.selectedOutput = + dbus.getDetachedOutput(dbus.activeSessionId, modelData.pid) + } + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + height: 1 + color: palette.mid + } + + // Output tail + Label { + text: processManagerWindow.selectedPid > 0 + ? "——— Output: pid " + processManagerWindow.selectedPid + " ———" + : "Select a process to view output" + opacity: 0.6 + font.pixelSize: Qt.application.font.pixelSize * 0.85 + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + TextArea { + readOnly: true + font.family: "monospace" + font.pixelSize: Qt.application.font.pixelSize * 0.9 + text: processManagerWindow.selectedOutput + wrapMode: TextEdit.Wrap + } + } + } + } } diff --git a/plasmoid/plugin/olliedbusclient.cpp b/plasmoid/plugin/olliedbusclient.cpp index 71f81f2..0d2f11d 100644 --- a/plasmoid/plugin/olliedbusclient.cpp +++ b/plasmoid/plugin/olliedbusclient.cpp @@ -57,6 +57,10 @@ OllieDBusClient::OllieDBusClient(QObject *parent) this, SLOT(onStateChanged(QString,QString))); bus.connect(SERVICE, OBJ_PATH, IFACE, "ChatUpdated", this, SLOT(onChatUpdated(QString,qlonglong,QString))); + bus.connect(SERVICE, OBJ_PATH, IFACE, "ProcessDetached", + this, SLOT(onProcessDetached(QString))); + bus.connect(SERVICE, OBJ_PATH, IFACE, "ProcessExited", + this, SLOT(onProcessExited(QString,int,int))); refreshSessions(); } @@ -442,6 +446,53 @@ bool OllieDBusClient::interrupt(const QString &sessionId) return true; } +// --- Detached process management --- + +bool OllieDBusClient::detachProcess(const QString &sessionId) +{ + QDBusReply reply = m_iface->call("DetachProcess", sessionId); + return reply.isValid() && reply.value(); +} + +QVariantList OllieDBusClient::listDetached(const QString &sessionId) +{ + QDBusReply reply = m_iface->call("ListDetached", sessionId); + if (!reply.isValid()) return {}; + + QVariantList result; + for (const QString &entry : reply.value()) { + QStringList parts = entry.split('\t'); + if (parts.size() >= 4) { + QVariantMap proc; + proc["pid"] = parts[0].toInt(); + proc["command"] = parts[1]; + proc["started"] = parts[2].toLongLong(); + proc["status"] = parts[3]; + result.append(proc); + } + } + return result; +} + +bool OllieDBusClient::signalDetached(const QString &sessionId, int pid, const QString &signal) +{ + QDBusReply reply = m_iface->call("SignalDetached", sessionId, pid, signal); + return reply.isValid() && reply.value(); +} + +QString OllieDBusClient::getDetachedOutput(const QString &sessionId, int pid) +{ + QDBusReply reply = m_iface->call("GetDetachedOutput", sessionId, pid); + if (reply.isValid()) return reply.value(); + return QString(); +} + +bool OllieDBusClient::dismissDetached(const QString &sessionId, int pid) +{ + QDBusReply reply = m_iface->call("DismissDetached", sessionId, pid); + return reply.isValid() && reply.value(); +} + // --- D-Bus signal handlers --- void OllieDBusClient::onSessionCreated(const QString &session_id) @@ -505,3 +556,13 @@ void OllieDBusClient::onChatUpdated(const QString &session_id, qlonglong offset, fetchChat(); } } + +void OllieDBusClient::onProcessDetached(const QString &session_id) +{ + emit processDetached(session_id); +} + +void OllieDBusClient::onProcessExited(const QString &session_id, int pid, int exitCode) +{ + emit processExited(session_id, pid, exitCode); +} diff --git a/plasmoid/plugin/olliedbusclient.h b/plasmoid/plugin/olliedbusclient.h index c79349a..c004f77 100644 --- a/plasmoid/plugin/olliedbusclient.h +++ b/plasmoid/plugin/olliedbusclient.h @@ -55,6 +55,13 @@ public: Q_INVOKABLE bool submit(const QString &sessionId, const QString &prompt); Q_INVOKABLE bool interrupt(const QString &sessionId); + // Detached process management + Q_INVOKABLE bool detachProcess(const QString &sessionId); + Q_INVOKABLE QVariantList listDetached(const QString &sessionId); + Q_INVOKABLE bool signalDetached(const QString &sessionId, int pid, const QString &signal); + Q_INVOKABLE QString getDetachedOutput(const QString &sessionId, int pid); + Q_INVOKABLE bool dismissDetached(const QString &sessionId, int pid); + signals: void sessionsChanged(); void activeSessionIdChanged(); @@ -65,6 +72,8 @@ signals: void activeBackendChanged(); void sessionStateUpdated(const QString &sessionId, const QString &newState); void chatAppended(const QString &sessionId, int offset, const QString &text); + void processDetached(const QString &sessionId); + void processExited(const QString &sessionId, int pid, int exitCode); private slots: void onSessionCreated(const QString &session_id); @@ -77,6 +86,8 @@ private slots: void onFetchChatFinished(QDBusPendingCallWatcher *watcher); void onFetchConfigFinished(QDBusPendingCallWatcher *watcher); void onPortalFolderResponse(uint response, const QVariantMap &results); + void onProcessDetached(const QString &session_id); + void onProcessExited(const QString &session_id, int pid, int exitCode); private: QDBusInterface *m_iface;