gui: add process detach support + Process Manager window

- OllieDBusClient: detachProcess, listDetached, signalDetached,
  getDetachedOutput, dismissDetached methods + D-Bus signal handlers
- ChatPane.qml: Detach button (visible during calling: state) +
  badge showing detached process count (click opens Process Manager)
- main.qml + main-kf5.qml: Process Manager window with process list,
  live output tail, Term/Kill/Dismiss buttons per process
This commit is contained in:
Levi Neely 2026-07-17 17:01:16 +02:00
parent 89dba79dc1
commit 131e875820
5 changed files with 401 additions and 0 deletions

View File

@ -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

View File

@ -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
}
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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<bool> reply = m_iface->call("DetachProcess", sessionId);
return reply.isValid() && reply.value();
}
QVariantList OllieDBusClient::listDetached(const QString &sessionId)
{
QDBusReply<QStringList> 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<bool> reply = m_iface->call("SignalDetached", sessionId, pid, signal);
return reply.isValid() && reply.value();
}
QString OllieDBusClient::getDetachedOutput(const QString &sessionId, int pid)
{
QDBusReply<QString> reply = m_iface->call("GetDetachedOutput", sessionId, pid);
if (reply.isValid()) return reply.value();
return QString();
}
bool OllieDBusClient::dismissDetached(const QString &sessionId, int pid)
{
QDBusReply<bool> 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);
}

View File

@ -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;