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:
parent
a0f71a01c2
commit
3cedf602be
149
gui/main.qml
149
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Reference in New Issue