incremental session tree updates

Instead of clearing and rebuilding the entire session tree model on
every sessionsChanged signal, diff old vs new and:
- Update existing session/agent properties in place
- Remove deleted sessions/agents
- Append new sessions/agents

This avoids O(n) model rebuilds that cause ListView to re-render all
delegates, improving responsiveness with many sessions.
This commit is contained in:
Levi Neely 2026-08-03 19:51:54 +02:00
parent 0c93414251
commit 24921e7a2b
1 changed files with 189 additions and 33 deletions

View File

@ -109,52 +109,203 @@ ApplicationWindow {
// Cache for session expanded state (sessionId -> bool)
property var sessionExpandedCache: ({})
// Cache of current model structure for diffing: { sessionId: { agents: [agentId, ...] } }
property var modelCache: ({})
function buildSessionTree() {
// Preserve focus if prompt input has it
// Full rebuild - used on initial load
var hadFocus = promptInput.activeFocus
sessionTreeModel.clear()
var newCache = {}
modelCache = {}
var sessions = ollie.sessions
for (var i = 0; i < sessions.length; ++i) {
var sess = sessions[i]
var sessionId = sess.id || ""
var sessionName = sess.name || sessionId
var agents = sess.agents || []
var agentCount = agents.length
var displayLabel = agentCount > 0 ? sessionName + " (" + agentCount + ")" : sessionName
// Preserve expanded state from cache, default to true
var wasExpanded = sessionExpandedCache[sessionId] !== undefined ? sessionExpandedCache[sessionId] : true
newCache[sessionId] = wasExpanded
appendSessionToModel(sess)
}
if (hadFocus)
promptInput.forceActiveFocus()
}
function appendSessionToModel(sess) {
var sessionId = sess.id || ""
var sessionName = sess.name || sessionId
var agents = sess.agents || []
var agentCount = agents.length
var displayLabel = agentCount > 0 ? sessionName + " (" + agentCount + ")" : sessionName
var wasExpanded = sessionExpandedCache[sessionId] !== undefined ? sessionExpandedCache[sessionId] : true
sessionExpandedCache[sessionId] = wasExpanded
sessionTreeModel.append({
type: "session",
id: sessionId,
level: 0,
expanded: wasExpanded,
state: sess.state || "",
model: sess.model || "",
displayName: displayLabel,
baseName: sessionName,
connectionColor: ollie.sessionConnectionColor(sessionId)
})
var agentIds = []
for (var j = 0; j < agents.length; ++j) {
var agent = agents[j]
var liveState = ollie.agentState(sessionId, agent.id)
sessionTreeModel.append({
type: "session",
id: sessionId,
level: 0,
expanded: wasExpanded,
state: sess.state || "",
model: sess.model || "",
displayName: displayLabel,
baseName: sessionName, // For rename operations
connectionColor: ollie.sessionConnectionColor(sessionId)
type: "agent",
parentId: sessionId,
agentId: agent.id,
level: 1,
state: liveState || agent.state || "idle",
connected: agent.connected,
displayName: agent.name
})
for (var j = 0; j < agents.length; ++j) {
var agent = agents[j]
// Use live state if available, otherwise fall back to refresh-time state
var liveState = ollie.agentState(sessionId, agent.id)
sessionTreeModel.append({
agentIds.push(agent.id)
}
modelCache[sessionId] = { agents: agentIds }
}
function updateSessionTree() {
// Incremental update - diff old vs new
var hadFocus = promptInput.activeFocus
var sessions = ollie.sessions
// Build lookup of new sessions
var newSessionIds = {}
for (var i = 0; i < sessions.length; ++i) {
newSessionIds[sessions[i].id] = sessions[i]
}
// Build lookup of old sessions from cache
var oldSessionIds = Object.keys(modelCache)
// Find removed sessions and delete from model (iterate backwards to preserve indices)
for (var idx = sessionTreeModel.count - 1; idx >= 0; --idx) {
var item = sessionTreeModel.get(idx)
if (item.type === "session" && !newSessionIds[item.id]) {
// Remove session and all its agents
removeSessionFromModel(idx, item.id)
}
}
// Update existing sessions and add new ones
for (var j = 0; j < sessions.length; ++j) {
var sess = sessions[j]
var sessionId = sess.id
if (modelCache[sessionId]) {
// Session exists - update properties
updateSessionInModel(sess)
} else {
// New session - append at end
appendSessionToModel(sess)
}
}
if (hadFocus)
promptInput.forceActiveFocus()
}
function findSessionIndex(sessionId) {
for (var i = 0; i < sessionTreeModel.count; ++i) {
var item = sessionTreeModel.get(i)
if (item.type === "session" && item.id === sessionId)
return i
}
return -1
}
function removeSessionFromModel(sessionIdx, sessionId) {
// Count agents to remove (they follow the session)
var removeCount = 1
for (var i = sessionIdx + 1; i < sessionTreeModel.count; ++i) {
var item = sessionTreeModel.get(i)
if (item.type === "session") break
if (item.type === "agent" && item.parentId === sessionId)
removeCount++
}
// Remove session + agents
for (var r = 0; r < removeCount; ++r) {
sessionTreeModel.remove(sessionIdx)
}
delete modelCache[sessionId]
delete sessionExpandedCache[sessionId]
}
function updateSessionInModel(sess) {
var sessionId = sess.id
var sessionIdx = findSessionIndex(sessionId)
if (sessionIdx < 0) return
var sessionName = sess.name || sessionId
var agents = sess.agents || []
var agentCount = agents.length
var displayLabel = agentCount > 0 ? sessionName + " (" + agentCount + ")" : sessionName
// Update session properties
sessionTreeModel.setProperty(sessionIdx, "displayName", displayLabel)
sessionTreeModel.setProperty(sessionIdx, "baseName", sessionName)
sessionTreeModel.setProperty(sessionIdx, "state", sess.state || "")
sessionTreeModel.setProperty(sessionIdx, "model", sess.model || "")
sessionTreeModel.setProperty(sessionIdx, "connectionColor", ollie.sessionConnectionColor(sessionId))
// Build agent lookup
var newAgentIds = {}
for (var i = 0; i < agents.length; ++i) {
newAgentIds[agents[i].id] = agents[i]
}
var oldAgentIds = modelCache[sessionId] ? modelCache[sessionId].agents : []
// Update/remove existing agents
for (var idx = sessionIdx + 1; idx < sessionTreeModel.count; ++idx) {
var item = sessionTreeModel.get(idx)
if (item.type === "session") break
if (item.type === "agent" && item.parentId === sessionId) {
if (newAgentIds[item.agentId]) {
// Update agent
var agent = newAgentIds[item.agentId]
var liveState = ollie.agentState(sessionId, agent.id)
sessionTreeModel.setProperty(idx, "state", liveState || agent.state || "idle")
sessionTreeModel.setProperty(idx, "connected", agent.connected)
sessionTreeModel.setProperty(idx, "displayName", agent.name)
} else {
// Remove agent
sessionTreeModel.remove(idx)
--idx // Adjust for removed item
}
}
}
// Add new agents (at end of session's agents)
var insertIdx = sessionIdx + 1
for (var k = insertIdx; k < sessionTreeModel.count; ++k) {
var checkItem = sessionTreeModel.get(k)
if (checkItem.type === "session") break
insertIdx = k + 1
}
var newAgentList = []
for (var m = 0; m < agents.length; ++m) {
var ag = agents[m]
newAgentList.push(ag.id)
var found = false
for (var n = 0; n < oldAgentIds.length; ++n) {
if (oldAgentIds[n] === ag.id) { found = true; break }
}
if (!found) {
var liveState2 = ollie.agentState(sessionId, ag.id)
sessionTreeModel.insert(insertIdx, {
type: "agent",
parentId: sessionId,
agentId: agent.id,
agentId: ag.id,
level: 1,
state: liveState || agent.state || "idle",
connected: agent.connected,
displayName: agent.name
state: liveState2 || ag.state || "idle",
connected: ag.connected,
displayName: ag.name
})
}
}
sessionExpandedCache = newCache
// Restore focus if it was on the prompt input
if (hadFocus)
promptInput.forceActiveFocus()
modelCache[sessionId] = { agents: newAgentList }
}
function toggleExpand(index) {
@ -218,7 +369,12 @@ ApplicationWindow {
Connections {
target: ollie
function onSessionsChanged() {
buildSessionTree()
// Use incremental update if model exists, full rebuild otherwise
if (sessionTreeModel.count > 0 && Object.keys(modelCache).length > 0) {
updateSessionTree()
} else {
buildSessionTree()
}
}
function onAgentStateChanged(sessionId, agentId, state) {
// Increment version to force re-evaluation of all agent state colors