gui: session tree with Active/Paused groups, arbitrary-depth expand/collapse
This commit is contained in:
parent
265a1fdda9
commit
22302ce9f1
105
gui/main.qml
105
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue