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:
parent
89dba79dc1
commit
131e875820
|
|
@ -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
|
||||
|
|
|
|||
163
gui/main-kf5.qml
163
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
135
gui/main.qml
135
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Reference in New Issue