import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window Window { id: procWindow title: "Background Processes" + (currentSessionName ? " — " + currentSessionName : "") width: 650 height: 450 minimumWidth: 400 minimumHeight: 300 property var procs: [] property string outputText: "" property int selectedPid: -1 property bool outputVisible: true // Track the session we're displaying (synced from main window) property string currentSessionId: ollie.activeSessionId property string currentAgentId: ollie.activeAgentId property string currentSessionName: "" function refresh() { if (!currentSessionId || !currentAgentId) { procs = [] return } var rawProcs = ollie.listProcs(currentSessionId, currentAgentId) // Sort by PID descending (newest first) rawProcs.sort(function(a, b) { return b.pid - a.pid }) procs = rawProcs } function updateSessionName() { if (currentSessionId) { var cfg = ollie.readSessionConfig(currentSessionId) currentSessionName = cfg.name || currentSessionId } else { currentSessionName = "" } } Timer { id: refreshTimer interval: 2000 running: procWindow.visible repeat: true onTriggered: refresh() } onVisibleChanged: { if (visible) { currentSessionId = ollie.activeSessionId currentAgentId = ollie.activeAgentId updateSessionName() refresh() } } // Sync to active session/agent changes Connections { target: ollie function onActiveSessionIdChanged() { if (procWindow.visible) { currentSessionId = ollie.activeSessionId currentAgentId = ollie.activeAgentId updateSessionName() selectedPid = -1 outputText = "" refresh() } } function onActiveAgentIdChanged() { if (procWindow.visible) { currentAgentId = ollie.activeAgentId selectedPid = -1 outputText = "" refresh() } } function onProcStarted(sessionId, agentId, payload) { if (procWindow.visible && sessionId === currentSessionId && agentId === currentAgentId) { refresh() } } } ColumnLayout { anchors.fill: parent anchors.margins: 12 spacing: 8 // Header showing current session/agent RowLayout { Layout.fillWidth: true spacing: 8 Label { text: "Session:" font.bold: true } Label { text: currentSessionName || "(none)" Layout.fillWidth: true elide: Text.ElideMiddle } } // Resizable split between process list and output SplitView { Layout.fillWidth: true Layout.fillHeight: true orientation: Qt.Vertical // Process list ListView { id: procList SplitView.fillWidth: true SplitView.minimumHeight: 80 SplitView.preferredHeight: 180 model: procs clip: true ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } delegate: Rectangle { width: procList.width height: 44 color: selectedPid === modelData.pid ? palette.highlight : (index % 2 ? palette.alternateBase : palette.base) MouseArea { anchors.fill: parent onClicked: selectedPid = modelData.pid onDoubleClicked: { selectedPid = modelData.pid var out = ollie.procOutput(currentSessionId, currentAgentId, selectedPid) outputText = out.length > 0 ? out : "(no output)" outputVisible = true } } RowLayout { anchors.fill: parent anchors.margins: 8 spacing: 12 // Status indicator Rectangle { width: 10 height: 10 radius: 5 color: modelData.status === "running" ? "#4CAF50" : (modelData.exitCode === 0 ? "#9E9E9E" : "#F44336") } // PID Label { text: modelData.pid font.family: "monospace" Layout.preferredWidth: 50 } // 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: 55 horizontalAlignment: Text.AlignRight } // Exit code (if exited) Label { visible: modelData.status !== "running" text: modelData.exitCode === 0 ? "✓" : "exit " + modelData.exitCode font.family: "monospace" color: modelData.exitCode === 0 ? "#4CAF50" : "#F44336" Layout.preferredWidth: 45 } } } Label { anchors.centerIn: parent visible: procs.length === 0 text: currentSessionId ? "No background processes" : "No session selected" opacity: 0.5 } } // Output pane (toggle-able) GroupBox { id: outputPane visible: outputVisible SplitView.fillWidth: true SplitView.fillHeight: true SplitView.minimumHeight: 60 title: selectedPid > 0 ? "Output (PID " + selectedPid + ")" : "Output" ScrollView { anchors.fill: parent TextArea { id: outputArea text: outputText readOnly: true font.family: "monospace" font.pointSize: 11 wrapMode: TextEdit.Wrap selectByMouse: true placeholderText: "Select a process and click View Output" } } } } // 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 = "" var out = ollie.procOutput(currentSessionId, currentAgentId, selectedPid) outputText = out.length > 0 ? out : "(no output)" outputVisible = true } } Button { text: outputVisible ? "Hide Output" : "Show Output" icon.name: outputVisible ? "go-up" : "go-down" onClicked: outputVisible = !outputVisible } Item { Layout.fillWidth: true } Button { text: "Terminate" icon.name: "process-stop" enabled: selectedPid > 0 && isRunning(selectedPid) onClicked: { ollie.procTerm(currentSessionId, currentAgentId, selectedPid) Qt.callLater(refresh) } } Button { text: "Kill" icon.name: "edit-delete" enabled: selectedPid > 0 && isRunning(selectedPid) onClicked: { ollie.procKill(currentSessionId, currentAgentId, selectedPid) Qt.callLater(refresh) } } Button { text: "Dismiss" icon.name: "window-close" onClicked: procWindow.close() } } } 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 } }