diff --git a/gui/main.qml b/gui/main.qml index e689b53..da7f4bf 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -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