gui: session tree with Active/Paused groups, arbitrary-depth expand/collapse

This commit is contained in:
Levi Neely 2026-08-04 16:05:08 +02:00
parent 265a1fdda9
commit 22302ce9f1
1 changed files with 83 additions and 22 deletions

View File

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