gui: session tree view with agent sub-elements

- 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
This commit is contained in:
Levi Neely 2026-08-01 11:43:55 +02:00
parent a0f71a01c2
commit 3cedf602be
3 changed files with 175 additions and 20 deletions

View File

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

View File

@ -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()

View File

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