diff --git a/gui/main.qml b/gui/main.qml index 6d149d9..6b0af3c 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -118,8 +118,37 @@ ApplicationWindow { sessionTreeModel.clear() modelCache = {} var sessions = ollie.sessions + + // Sessions are already sorted: active first, paused last. + // Insert group headers when paused state changes. + var lastPaused = null for (var i = 0; i < sessions.length; ++i) { var sess = sessions[i] + var isPaused = sess.paused || false + + // Insert header on transition + if (lastPaused === null && !isPaused) { + sessionTreeModel.append({ + type: "header", + level: 0, + displayName: "Active" + }) + } else if (lastPaused === false && isPaused) { + sessionTreeModel.append({ + type: "header", + level: 0, + displayName: "Inactive" + }) + } else if (lastPaused === null && isPaused) { + // All sessions are paused - show Inactive header + sessionTreeModel.append({ + type: "header", + level: 0, + displayName: "Inactive" + }) + } + lastPaused = isPaused + appendSessionToModel(sess) } if (hadFocus) @@ -168,7 +197,8 @@ ApplicationWindow { level: 1, state: agent.state || "idle", connected: agent.connected, - displayName: agent.name + displayName: agent.name, + parentExpanded: wasExpanded }) agentIds.push(agent.id) } @@ -319,11 +349,41 @@ ApplicationWindow { function toggleExpand(index) { var item = sessionTreeModel.get(index) - if (item.type === "session") { - var newExpanded = !item.expanded - sessionTreeModel.set(index, { expanded: newExpanded }) + if (!item.expanded === undefined) return // only expandable items + + var newExpanded = !item.expanded + var itemLevel = item.level || 0 + sessionTreeModel.setProperty(index, "expanded", newExpanded) + + // Cache for sessions (by id) + if (item.type === "session" && item.id) { sessionExpandedCache[item.id] = newExpanded } + + // Update all descendants: items with level > itemLevel until we hit same or lower level + for (var i = index + 1; i < sessionTreeModel.count; ++i) { + var child = sessionTreeModel.get(i) + var childLevel = child.level || 0 + if (childLevel <= itemLevel) break // end of descendants + + // Determine visibility: walk up to find if any ancestor is collapsed + var visible = newExpanded + if (visible) { + // Check all ancestors between this item and the child + for (var j = i - 1; j >= index; --j) { + var ancestor = sessionTreeModel.get(j) + var ancestorLevel = ancestor.level || 0 + if (ancestorLevel < childLevel && ancestor.expanded === false) { + visible = false + break + } + if (ancestorLevel < childLevel) { + childLevel = ancestorLevel // track the level we've checked up to + } + } + } + sessionTreeModel.setProperty(i, "parentExpanded", visible) + } } function activateAgent(parentId, agentId) { @@ -378,12 +438,8 @@ ApplicationWindow { Connections { target: ollie function onSessionsChanged() { - // Use incremental update if model exists, full rebuild otherwise - if (sessionTreeModel.count > 0 && Object.keys(modelCache).length > 0) { - updateSessionTree() - } else { - buildSessionTree() - } + // Always do full rebuild since headers complicate incremental updates + buildSessionTree() } function onAgentStateChanged(sessionId, agentId, state) { // Update only the specific agent in the model @@ -889,17 +945,23 @@ ApplicationWindow { // Guard against undefined during initial render height: { if (!type) return 0 - if (type === "agent" && !parentSessionExpanded(parentId)) + if (type === "header") return 24 + if (type === "agent" && !parentExpanded) return 0 return 28 } visible: height > 0 property int modelIndex: index + property bool isHeader: type === "header" + + // Headers are not interactive + enabled: !isHeader MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton hoverEnabled: false + enabled: !parent.isHeader onClicked: function(mouse) { if (mouse.button === Qt.RightButton) { if (type === "session") { @@ -995,8 +1057,8 @@ ApplicationWindow { // Indent + expand/collapse indicator Item { - Layout.preferredWidth: (level || 0) * 16 + 16 - Layout.preferredHeight: 28 + Layout.preferredWidth: type === "header" ? 8 : (level || 0) * 16 + 16 + Layout.preferredHeight: type === "header" ? 24 : 28 Label { anchors.centerIn: parent @@ -1004,13 +1066,14 @@ ApplicationWindow { font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10 color: theme.textColor opacity: 0.6 + visible: type !== "header" } } // Name area Item { Layout.fillWidth: true - Layout.preferredHeight: 28 + Layout.preferredHeight: type === "header" ? 24 : 28 TextField { anchors.fill: parent @@ -1043,20 +1106,23 @@ ApplicationWindow { Label { anchors.fill: parent text: displayName || "" - color: theme.textColor - font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize + color: type === "header" ? theme.midColor : theme.textColor + font.pointSize: theme.fontSize > 0 ? theme.fontSize * (type === "header" ? 0.85 : 1) : Qt.application.font.pointSize + font.bold: type === "header" + font.capitalization: type === "header" ? Font.AllUppercase : Font.MixedCase verticalAlignment: Text.AlignVCenter visible: modelIndex !== renamingIndex } } - // State dot + // State dot (hidden for headers) Rectangle { id: stateDot Layout.preferredWidth: 8 Layout.preferredHeight: 8 Layout.alignment: Qt.AlignVCenter radius: 4 + visible: type !== "header" color: { if (type === "session") { // Session: RPC connection health @@ -1076,11 +1142,6 @@ ApplicationWindow { } highlighted: (type === "agent" && agentId === ollie.activeAgentId && parentId === ollie.activeSessionId) - - function parentSessionExpanded(pid) { - // O(1) lookup using cache instead of O(n) model scan - return sessionExpandedCache[pid] !== undefined ? sessionExpandedCache[pid] : true - } } } }