From 3cedf602bebcf7b396968f817802e98e846f5a71 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:43:55 +0200 Subject: [PATCH] gui: session tree view with agent sub-elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add getAgents() to Ollie9pClient for listing agents per session - Add switchAgent() for switching sessions+agents atomically - Add activeAgentId property and activeAgentIdChanged signal - Replace flat ListView with tree-aware model (session → agents) - Session nodes expandable/collapsible with ▶/▼ indicator - Agent nodes indented, clickable to activate that agent - Active agent highlighted, active session auto-expanded - State dots on both session and agent rows --- gui/main.qml | 149 ++++++++++++++++++++++++++++++++++++------ gui/ollie9pclient.cpp | 40 ++++++++++++ gui/ollie9pclient.h | 6 ++ 3 files changed, 175 insertions(+), 20 deletions(-) diff --git a/gui/main.qml b/gui/main.qml index db1392b..428d363 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -67,6 +67,63 @@ ApplicationWindow { onTriggered: chatView.positionViewAtEnd() } + // Session tree model — flat list with level info + ListModel { + id: sessionTreeModel + } + + function buildSessionTree() { + sessionTreeModel.clear() + var sessions = ollie.sessions + for (var i = 0; i < sessions.length; ++i) { + var sess = sessions[i].toMap() + var sid = sess.id || "" + // Auto-expand active session + var expanded = (sid === ollie.activeSessionId) + // Add session node + sessionTreeModel.append({ + type: "session", + id: sid, + level: 0, + expanded: expanded, + state: sess.state || "", + model: sess.model || "", + displayName: sid + }) + // Fetch agents and add them + var agents = ollie.getAgents(sid) + for (var j = 0; j < agents.length; ++j) { + var aid = agents[j] + sessionTreeModel.append({ + type: "agent", + parentId: sid, + agentId: aid, + level: 1, + state: "idle", + displayName: "Agent " + aid + }) + } + } + } + + function toggleExpand(index) { + var item = sessionTreeModel.get(index) + if (item.type === "session") { + sessionTreeModel.set(index, { expanded: !item.expanded }) + } + } + + function activateAgent(parentId, agentId) { + ollie.switchAgent(parentId, agentId) + } + + Connections { + target: ollie + function onSessionsChanged() { + buildSessionTree() + } + } + Component.onCompleted: { var log = ollie.readLog() if (log) { @@ -86,6 +143,8 @@ ApplicationWindow { chatModel.trimFront(30) } scrollTimer.start() + // Expand the active session in the tree + buildSessionTree() } function onStreamingDone() { chatModel.appendData("\n[info]\nsession ended\n") @@ -241,34 +300,84 @@ ApplicationWindow { id: sessionList Layout.fillWidth: true Layout.fillHeight: true - model: ollie.sessions + model: sessionTreeModel clip: true delegate: ItemDelegate { width: sessionList.width - text: modelData.id || "" - highlighted: (modelData.id === ollie.activeSessionId) - onClicked: ollie.activeSessionId = modelData.id - contentItem: Label { - text: modelData.id || "" - color: theme.textColor - font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize - verticalAlignment: Text.AlignVCenter - leftPadding: 8 + height: { + if (modelData.type === "agent" && !parentSessionExpanded(modelData.parentId)) + return 0 + return 28 + } + visible: height > 0 + + property var _parentItem: null + + contentItem: RowLayout { + anchors.fill: parent + spacing: 0 + + // Expand/collapse indicator + Label { + Layout.leftPadding: modelData.level * 16 + 4 + Layout.preferredWidth: 16 + text: modelData.type === "session" ? (modelData.expanded ? "▼" : "▶") : "" + font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10 + color: theme.textColor + opacity: 0.6 + } + + // Icon/type indicator + Label { + Layout.preferredWidth: 16 + text: modelData.type === "session" ? "📁" : "🤖" + font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.8 : 11 + } + + // Name + Label { + Layout.fillWidth: true + text: modelData.displayName || "" + color: theme.textColor + font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize + verticalAlignment: Text.AlignVCenter + leftPadding: 4 + } + + // State dot + Rectangle { + Layout.preferredWidth: 8 + Layout.preferredHeight: 8 + radius: 4 + anchors.verticalCenter: parent.verticalCenter + color: { + var s = modelData.state || "idle" + if (s === "idle") return "#4caf50" + if (s.startsWith("calling")) return "#ff9800" + return "#2196f3" + } + } } - Rectangle { - width: 8; height: 8; radius: 4 - anchors.right: parent.right - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - color: { - var s = modelData.state || "idle" - if (s === "idle") return "#4caf50" - if (s.startsWith("calling")) return "#ff9800" - return "#2196f3" + onClicked: { + if (modelData.type === "session") { + toggleExpand(index) + } else if (modelData.type === "agent") { + activateAgent(modelData.parentId, modelData.agentId) } } + + highlighted: (modelData.type === "agent" && modelData.agentId === ollie.activeAgentId && modelData.parentId === ollie.activeSessionId) + + function parentSessionExpanded(parentId) { + for (var i = 0; i < sessionTreeModel.count; ++i) { + var item = sessionTreeModel.get(i) + if (item.id === parentId && item.type === "session") + return item.expanded + } + return false + } } } } diff --git a/gui/ollie9pclient.cpp b/gui/ollie9pclient.cpp index 0f68513..305e9c3 100644 --- a/gui/ollie9pclient.cpp +++ b/gui/ollie9pclient.cpp @@ -151,6 +151,46 @@ QString Ollie9pClient::getConfig() return QString::fromUtf8(out); } +QStringList Ollie9pClient::getAgents(const QString &sessionId) +{ + if (sessionId.isEmpty()) return {}; + QByteArray out = run9p({"ls", "session/" + sessionId + "/agent"}); + QString raw = QString::fromUtf8(out).trimmed(); + if (raw.isEmpty()) return {"0"}; // default fallback + QStringList agents = raw.split('\n', Qt::SkipEmptyParts); + for (int i = 0; i < agents.size(); ++i) + agents[i] = agents[i].trimmed(); + return agents; +} + +void Ollie9pClient::setActiveAgentId(const QString &agentId) +{ + m_agentId = agentId; +} + +void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId) +{ + if (m_activeSessionId == sessionId && m_agentId == agentId) return; + + stopStreams(); + m_activeSessionId = sessionId; + m_agentId = agentId; + m_activeState = "idle"; + + // Read initial state immediately + QByteArray stateOut = run9p({"read", agentPath() + "/state"}); + QString stateStr = QString::fromUtf8(stateOut).trimmed(); + if (!stateStr.isEmpty()) + m_activeState = stateStr; + + emit activeSessionIdChanged(); + emit activeAgentIdChanged(); + emit activeStateChanged(); + + startChatStream(); + startStateStream(); +} + // --- Streaming --- void Ollie9pClient::startChatStream() diff --git a/gui/ollie9pclient.h b/gui/ollie9pclient.h index 78c00a2..fe94ab7 100644 --- a/gui/ollie9pclient.h +++ b/gui/ollie9pclient.h @@ -20,6 +20,7 @@ class Ollie9pClient : public QObject Q_PROPERTY(QVariantList sessions READ sessions NOTIFY sessionsChanged) Q_PROPERTY(QString activeSessionId READ activeSessionId WRITE setActiveSessionId NOTIFY activeSessionIdChanged) + Q_PROPERTY(QString activeAgentId READ activeAgentId NOTIFY activeAgentIdChanged) Q_PROPERTY(QString activeState READ activeState NOTIFY activeStateChanged) public: @@ -29,6 +30,7 @@ public: QVariantList sessions() const { return m_sessions; } QString activeSessionId() const { return m_activeSessionId; } void setActiveSessionId(const QString &id); + QString activeAgentId() const { return m_agentId; } QString activeState() const { return m_activeState; } Q_INVOKABLE void refreshSessions(); @@ -37,10 +39,14 @@ public: Q_INVOKABLE bool interrupt(); Q_INVOKABLE bool kill(); Q_INVOKABLE QString getConfig(); + Q_INVOKABLE QStringList getAgents(const QString &sessionId); + Q_INVOKABLE void setActiveAgentId(const QString &agentId); + Q_INVOKABLE void switchAgent(const QString &sessionId, const QString &agentId); signals: void sessionsChanged(); void activeSessionIdChanged(); + void activeAgentIdChanged(); void activeStateChanged(); void chatReceived(const QString &text); void streamingDone(); // chat process exited (session killed)