diff --git a/Makefile b/Makefile
index 3ce1a43..75458a7 100644
--- a/Makefile
+++ b/Makefile
@@ -41,6 +41,8 @@ tools:
go build -o $(CONFIG_DIR)/tools/$$name ./$$d; \
done
install -m644 data/tools/*.meta $(CONFIG_DIR)/tools/
+ for f in data/tools/*; do [ -f "$$f" ] && [ -x "$$f" ] && install -m755 "$$f" $(CONFIG_DIR)/tools/; done; true
+ cp -a data/tools/_lib $(CONFIG_DIR)/tools/
kde:
cmake -S kde -B kde/build -DCMAKE_INSTALL_PREFIX=$(PREFIX)
diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go
index dd06d37..725baec 100644
--- a/cmd/olliesrv/internal/fs/spec.go
+++ b/cmd/olliesrv/internal/fs/spec.go
@@ -494,6 +494,9 @@ func buildSessionChildren(
var parent *agent.Agent
if req.ParentID != "" {
parent = s.FindAgent(req.ParentID)
+ if parent == nil {
+ return nil, fmt.Errorf("parent agent %q not found", req.ParentID)
+ }
}
if parent != nil {
diff --git a/cmd/olliesrv/internal/session/registry.go b/cmd/olliesrv/internal/session/registry.go
index 50bc0f3..4dfc129 100644
--- a/cmd/olliesrv/internal/session/registry.go
+++ b/cmd/olliesrv/internal/session/registry.go
@@ -239,6 +239,7 @@ func waitIdle(poll, timeout time.Duration) {
}
// BuildIndex builds the session index content.
+// Format: session-id\tsession-name\tpaused\tconnected\tremote\tcwd
func BuildIndex() []byte {
var sb strings.Builder
mu.RLock()
@@ -265,31 +266,19 @@ func BuildIndex() []byte {
sess.mu.RLock()
connected := sess.toolsConn != nil && !sess.paused
sess.mu.RUnlock()
- var cwd, be, model string
- var agentList []string
- for _, ag := range sess.Agents() {
- if cwd == "" {
- cwd = ag.Cwd()
- }
- if be == "" {
- be = ag.BackendName()
- model = ag.ModelName()
- }
- agentList = append(agentList, fmt.Sprintf("%s:%s:%s", ag.Name(), ag.ID(), ag.State()))
- }
- agents := strings.Join(agentList, ";")
- fmt.Fprintf(&sb, "%s\t%s\t%t\t%t\t%s\t%s\t%s\t%s\n",
- e.name, sess.ID, e.paused, connected, cwd, be, model, agents)
+ fmt.Fprintf(&sb, "%s\t%s\t%t\t%t\t%s\t%s\n",
+ sess.ID, e.name, e.paused, connected, sess.Remote, sess.Cwd())
}
return []byte(sb.String())
}
// BuildAgentIndex builds the agent index for a session.
+// Format: session-id\tagent-id\tagent-name\tparent-id\tdepth\tstate
func BuildAgentIndex(sess *Session) []byte {
var sb strings.Builder
for _, ag := range sess.Agents() {
- fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\n",
- ag.Name(), ag.ID(), ag.Cwd(), ag.BackendName(), ag.ModelName())
+ fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%d\t%s\n",
+ sess.ID, ag.ID(), ag.Name(), ag.ParentID(), ag.Depth(), ag.State())
}
return []byte(sb.String())
}
diff --git a/data/tools/subagent_spawn b/data/tools/subagent_spawn
index 3ee89c0..1616bbf 100755
--- a/data/tools/subagent_spawn
+++ b/data/tools/subagent_spawn
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# args_json: {"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"Task prompt for the subagent"},"name":{"type":"string","description":"Subagent display name (optional)"},"profile":{"type":"string","description":"Agent profile (optional, default: default)"},"backend":{"type":"string","description":"Backend (optional)"},"model":{"type":"string","description":"Model (optional)"},"cwd":{"type":"string","description":"Working directory (optional, default: current)"}}}
+# args_json: {"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"Task prompt for the subagent"},"name":{"type":"string","description":"Subagent display name (optional)"},"profile":{"type":"string","description":"Agent profile (optional, default: default)"},"backend":{"type":"string","description":"Backend (optional)"},"model":{"type":"string","description":"Model (optional)"},"cwd":{"type":"string","description":"Working directory (optional, default: current)"},"parent":{"type":"string","description":"Parent agent ID (internal, auto-filled)"}}}
# ollie:prompt
# ## subagent_spawn
#
@@ -25,6 +25,7 @@ profile=$(arg_get "profile" "default")
backend=$(arg_get "backend" "")
model=$(arg_get "model" "")
cwd=$(arg_get "cwd" "$(pwd)")
+parent=$(arg_get "parent" "${OLLIE_UNAME:-}")
# Escape newlines in prompt for the key=value wire format.
escaped_prompt=$(printf '%s' "$prompt_text" | sed 's/\\/\\\\/g; s/$/\\n/' | tr -d '\n' | sed 's/\\n$//')
@@ -32,10 +33,11 @@ escaped_prompt=$(printf '%s' "$prompt_text" | sed 's/\\/\\\\/g; s/$/\\n/' | tr -
# Build the payload.
payload="cwd=${cwd}
profile=${profile}
-parent=${OLLIE_UNAME}
timeout=600
prompt=${escaped_prompt}"
+[ -n "$parent" ] && payload="${payload}
+parent=${parent}"
[ -n "$name" ] && payload="${payload}
name=${name}"
[ -n "$backend" ] && payload="${payload}
diff --git a/kde/gui/GoalDialog.qml b/kde/gui/GoalDialog.qml
new file mode 100644
index 0000000..cd7e4a4
--- /dev/null
+++ b/kde/gui/GoalDialog.qml
@@ -0,0 +1,106 @@
+// GoalDialog.qml — Dialog for setting a session goal
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+Dialog {
+ id: root
+ title: "Set Goal"
+ width: 500
+ height: 300
+ modal: false
+ x: (parent ? (parent.width - width) / 2 : 0)
+ y: (parent ? (parent.height - height) / 2 : 0)
+
+ property string sessionId: ""
+ property string sessionName: ""
+
+ background: Item {
+ Rectangle {
+ x: 4
+ y: 6
+ width: parent.width
+ height: parent.height
+ color: Qt.rgba(0, 0, 0, 0.28)
+ radius: 4
+ }
+ Rectangle {
+ anchors.fill: parent
+ color: theme.windowColor
+ border.color: theme.dialogOutlineColor
+ border.width: 3
+ radius: 4
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.margins: 3
+ color: "transparent"
+ border.color: theme.lightColor
+ border.width: 1
+ radius: 3
+ }
+ }
+ }
+
+ header: Rectangle {
+ implicitHeight: 36
+ color: theme.buttonColor
+ Text {
+ anchors.centerIn: parent
+ text: root.title
+ color: theme.buttonTextColor
+ }
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.SizeAllCursor
+ property point pressPosition
+ onPressed: pressPosition = Qt.point(mouse.x, mouse.y)
+ onPositionChanged: if (pressed) {
+ root.x += mouse.x - pressPosition.x
+ root.y += mouse.y - pressPosition.y
+ }
+ }
+ }
+
+ contentItem: ColumnLayout {
+ spacing: 8
+ Layout.margins: 16
+
+ ScrollView {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ TextArea {
+ id: goalText
+ placeholderText: "Describe what you want to accomplish..."
+ wrapMode: TextEdit.Wrap
+ }
+ }
+ }
+
+ footer: RowLayout {
+ Layout.fillWidth: true
+ Item { Layout.fillWidth: true }
+ Button {
+ text: "OK"
+ onClicked: {
+ var text = goalText.text.trim()
+ if (text !== "" && root.sessionName !== "") {
+ ollie.setGoal(root.sessionName, text)
+ }
+ root.close()
+ }
+ }
+ Button {
+ text: "Cancel"
+ onClicked: root.close()
+ }
+ }
+
+ focus: false
+
+ onOpened: {
+ goalText.text = ""
+ goalText.forceActiveFocus()
+ }
+}
diff --git a/kde/gui/SessionTree.qml b/kde/gui/SessionTree.qml
index fd562e9..a56802b 100644
--- a/kde/gui/SessionTree.qml
+++ b/kde/gui/SessionTree.qml
@@ -8,11 +8,13 @@ Item {
property int renamingIndex: -1
property string renamingText: ""
property string killTargetSessionId: ""
+ property string killTargetSessionName: ""
property string killTargetAgentSessionId: ""
property string killTargetAgentId: ""
signal newSessionRequested()
signal settingsRequested()
signal newAgentRequested(string sessionId)
+ signal goalRequested(string sessionId, string sessionName)
signal renameFailed(string itemType, string attemptedName)
ColumnLayout {
@@ -94,8 +96,10 @@ Item {
if (mouse.button === Qt.RightButton) {
if (type === "session") {
var item = sessionModel.get(parent.modelIndex)
- if (item)
+ if (item) {
tree.killTargetSessionId = item.id
+ tree.killTargetSessionName = item.name
+ }
sessionCtxMenu.popup()
} else if (type === "agent") {
var agentItem = sessionModel.get(parent.modelIndex)
@@ -128,6 +132,10 @@ Item {
sessionModel.setExpanded(parent.modelIndex, !expanded)
} else if (type === "agent") {
ollie.switchAgent(parentId, agentId)
+ // Toggle expansion if agent has children
+ if (hasChildren) {
+ sessionModel.setExpanded(parent.modelIndex, !expanded)
+ }
}
}
onDoubleClicked: {
@@ -138,6 +146,12 @@ Item {
Menu {
id: sessionCtxMenu
+ MenuItem {
+ text: "Goal..."
+ onClicked: {
+ tree.goalRequested(tree.killTargetSessionId, tree.killTargetSessionName)
+ }
+ }
MenuItem {
text: "New Agent..."
onClicked: {
@@ -189,7 +203,7 @@ Item {
Label {
anchors.centerIn: parent
- text: type === "session" ? (expanded ? "▼" : "▶") : ""
+ text: (type === "session" || hasChildren) ? (expanded ? "▼" : "▶") : ""
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
color: theme.textColor
opacity: 0.6
diff --git a/kde/gui/main.qml b/kde/gui/main.qml
index f0592bc..d0a3c54 100644
--- a/kde/gui/main.qml
+++ b/kde/gui/main.qml
@@ -164,6 +164,11 @@ ApplicationWindow {
targetSessionId: ""
}
+ // --- Goal Dialog ---
+ GoalDialog {
+ id: goalDialog
+ }
+
SplitView {
anchors.fill: parent
@@ -178,6 +183,11 @@ ApplicationWindow {
newAgentDialog.targetSessionId = sessionId
newAgentDialog.open()
}
+ onGoalRequested: function(sessionId, sessionName) {
+ goalDialog.sessionId = sessionId
+ goalDialog.sessionName = sessionName
+ goalDialog.open()
+ }
onRenameFailed: function(itemType, attemptedName) {
renameErrorDialog.itemType = itemType
renameErrorDialog.attemptedName = attemptedName
diff --git a/kde/gui/ollie9pclient.cpp b/kde/gui/ollie9pclient.cpp
index 4b8cf22..1a863cf 100644
--- a/kde/gui/ollie9pclient.cpp
+++ b/kde/gui/ollie9pclient.cpp
@@ -263,6 +263,7 @@ void Ollie9pClient::refreshSessions()
if (!m_daemonConnected) return;
if (!m_9p || !m_9p->isConnected()) return;
+ // Read session index: session-id\tsession-name\tpaused\tconnected\tremote\tcwd
QByteArray out = m_9p->read(QStringLiteral("session/idx"));
QString raw = QString::fromUtf8(out);
@@ -275,12 +276,14 @@ void Ollie9pClient::refreshSessions()
if (!raw.isEmpty()) {
for (const QString &line : raw.split('\n', Qt::SkipEmptyParts)) {
QStringList parts = line.split('\t');
- // New format: name\tid\tpaused\tconnected\tcwd\tbackend\tmodel\tagents
- // Where agents is semicolon-separated: name:id:state;name:id:state
- if (parts.size() < 2) continue;
+ if (parts.size() < 4) continue;
- const QString sessionId = parts.value(1);
+ const QString sessionId = parts.value(0);
+ const QString sessionName = parts.value(1);
+ const bool isPaused = (parts.value(2) == "true");
const bool isConnected = (parts.value(3) == "true");
+ const QString remote = parts.value(4);
+ const QString cwd = parts.value(5);
const bool isActiveSession = (sessionId == m_activeSessionId);
if (isActiveSession) {
@@ -288,32 +291,35 @@ void Ollie9pClient::refreshSessions()
}
QVariantMap session;
- session["name"] = parts.value(0);
session["id"] = sessionId;
- session["paused"] = (parts.value(2) == "true");
+ session["name"] = sessionName;
+ session["paused"] = isPaused;
session["connected"] = isConnected;
- session["cwd"] = parts.value(4);
- session["backend"] = parts.value(5);
- session["model"] = parts.value(6);
+ session["remote"] = remote;
+ session["cwd"] = cwd;
- // Parse agents from semicolon-separated list
+ // Read agent index for this session: session-id\tagent-id\tagent-name\tparent-id\tdepth\tstate
QVariantList agents;
- QString agentStr = parts.value(7);
- if (!agentStr.isEmpty()) {
- for (const QString &agentEntry : agentStr.split(';', Qt::SkipEmptyParts)) {
- QStringList agentParts = agentEntry.split(':');
- if (agentParts.size() >= 3) {
- const QString agentId = agentParts.value(1);
- QVariantMap av;
- av["name"] = agentParts.value(0);
- av["id"] = agentId;
- av["state"] = agentParts.value(2);
- agents.append(av);
+ QString agentIdxPath = "session/" + sessionId + "/agent/idx";
+ QByteArray agentOut = m_9p->read(agentIdxPath);
+ QString agentRaw = QString::fromUtf8(agentOut);
+ if (!agentRaw.isEmpty()) {
+ for (const QString &agentLine : agentRaw.split('\n', Qt::SkipEmptyParts)) {
+ QStringList agentParts = agentLine.split('\t');
+ if (agentParts.size() < 5) continue;
- // Check if this is the active agent
- if (isActiveSession && agentId == m_agentId) {
- activeAgentFound = true;
- }
+ const QString agentId = agentParts.value(1);
+ QVariantMap av;
+ av["id"] = agentId;
+ av["name"] = agentParts.value(2);
+ av["parentAgentId"] = agentParts.value(3);
+ av["depth"] = agentParts.value(4).toInt();
+ av["state"] = agentParts.value(5);
+ agents.append(av);
+
+ // Check if this is the active agent
+ if (isActiveSession && agentId == m_agentId) {
+ activeAgentFound = true;
}
}
}
@@ -354,8 +360,17 @@ void Ollie9pClient::refreshSessions()
if (session.value("id").toString() != m_activeSessionId) continue;
const QVariantList agents = session.value("agents").toList();
if (!agents.isEmpty()) {
- const QString aid = agents.first().toMap().value("id").toString();
- if (!aid.isEmpty()) switchAgent(m_activeSessionId, aid);
+ // Select first top-level agent (depth 0)
+ for (const QVariant &av : agents) {
+ const QVariantMap agent = av.toMap();
+ if (agent.value("depth").toInt() == 0) {
+ const QString aid = agent.value("id").toString();
+ if (!aid.isEmpty()) {
+ switchAgent(m_activeSessionId, aid);
+ break;
+ }
+ }
+ }
}
break;
}
@@ -367,9 +382,17 @@ void Ollie9pClient::refreshSessions()
const QString sid = firstSession.value("id").toString();
const QVariantList agents = firstSession.value("agents").toList();
if (!agents.isEmpty()) {
- // Select first agent of first session
- const QString aid = agents.first().toMap().value("id").toString();
- switchAgent(sid, aid);
+ // Select first top-level agent (depth 0)
+ for (const QVariant &av : agents) {
+ const QVariantMap agent = av.toMap();
+ if (agent.value("depth").toInt() == 0) {
+ const QString aid = agent.value("id").toString();
+ if (!aid.isEmpty()) {
+ switchAgent(sid, aid);
+ break;
+ }
+ }
+ }
} else {
// No agents - just select the session
setActiveSessionId(sid);
@@ -603,6 +626,15 @@ QString Ollie9pClient::createSession(const QString &name, const QString &remote,
return QString(); // success
}
+void Ollie9pClient::setGoal(const QString &sessionName, const QString &goalText)
+{
+ if (!m_9p || !m_9p->isConnected()) return;
+ if (sessionName.isEmpty() || goalText.isEmpty()) return;
+
+ QString path = "session/" + sessionName + "/goal";
+ m_9p->write(path, goalText.toUtf8());
+}
+
QString Ollie9pClient::createAgent(const QString &sessionId, const QString &cwd, const QString &backend, const QString &model, const QString &agent, const QString &remote, const QString &agentAlias)
{
if (sessionId.isEmpty() || cwd.isEmpty()) return QStringLiteral("session ID and directory are required");
diff --git a/kde/gui/ollie9pclient.h b/kde/gui/ollie9pclient.h
index 7d7e4e2..ffe8dbd 100644
--- a/kde/gui/ollie9pclient.h
+++ b/kde/gui/ollie9pclient.h
@@ -86,6 +86,7 @@ public:
Q_INVOKABLE QStringList getAvailableModels(const QString &backend) const;
Q_INVOKABLE bool createQuickSession();
Q_INVOKABLE QString createSession(const QString &name, const QString &remote, const QString &workflow = QString());
+ Q_INVOKABLE void setGoal(const QString &sessionName, const QString &goalText);
Q_INVOKABLE QString createAgent(const QString &sessionId, const QString &cwd, const QString &backend, const QString &model, const QString &agent, const QString &remote, const QString &agentAlias);
Q_INVOKABLE bool killAgent(const QString &sessionId, const QString &agentId);
Q_INVOKABLE bool renameSession(const QString &sessionId, const QString &newName);
diff --git a/kde/gui/qml.qrc b/kde/gui/qml.qrc
index a491fee..1060c65 100644
--- a/kde/gui/qml.qrc
+++ b/kde/gui/qml.qrc
@@ -4,6 +4,7 @@
SettingsDialog.qml
NewSessionDialog.qml
NewAgentDialog.qml
+ GoalDialog.qml
ChatBlockDelegate.qml
SessionTree.qml
ChatPane.qml
diff --git a/kde/gui/sessionmodel.cpp b/kde/gui/sessionmodel.cpp
index 7480926..78e3683 100644
--- a/kde/gui/sessionmodel.cpp
+++ b/kde/gui/sessionmodel.cpp
@@ -26,6 +26,7 @@ QVariant SessionModel::data(const QModelIndex &index, int role) const
case LevelRole: return r.level;
case ExpandedRole: return r.expanded;
case ParentExpandedRole: return r.parentExpanded;
+ case HasChildrenRole: return r.hasChildren;
case StateRole: return r.state;
case ModelRole: return r.model;
case DisplayNameRole: return r.displayName;
@@ -40,7 +41,8 @@ QHash SessionModel::roleNames() const
{
return {{TypeRole, "type"}, {IdRole, "id"}, {ParentIdRole, "parentId"},
{AgentIdRole, "agentId"}, {LevelRole, "level"}, {ExpandedRole, "expanded"},
- {ParentExpandedRole, "parentExpanded"}, {StateRole, "state"}, {ModelRole, "model"},
+ {ParentExpandedRole, "parentExpanded"}, {HasChildrenRole, "hasChildren"},
+ {StateRole, "state"}, {ModelRole, "model"},
{DisplayNameRole, "displayName"}, {BaseNameRole, "baseName"},
{ConnectionColorRole, "connectionColor"}, {PausedRole, "paused"}};
}
@@ -65,21 +67,61 @@ void SessionModel::appendSession(const QVariantMap &session)
Row row;
row.type = "session"; row.id = id; row.expanded = expanded; row.paused = paused;
- row.state = session.value("state").toString(); row.model = session.value("model").toString();
row.baseName = name;
row.displayName = agents.isEmpty() ? name : name + " (" + QString::number(agents.size()) + ")";
row.connectionColor = connectionColor(session);
m_rows.append(row);
+ // Build agent tree starting from top-level agents (no parent)
+ appendAgentTree(id, agents, QString(), 1, expanded, paused);
+}
+
+void SessionModel::appendAgentTree(const QString &sessionId, const QVariantList &agents,
+ const QString &parentAgentId, int level, bool parentExpanded, bool paused)
+{
+ // Find all agents at this level (with matching parentAgentId)
for (const QVariant &value : agents) {
const QVariantMap agent = value.toMap();
+ const QString agentParent = agent.value("parentAgentId").toString();
+
+ // Check if this agent belongs at this level
+ if (agentParent != parentAgentId) continue;
+
+ const QString agentId = agent.value("id").toString();
+
+ // Check if this agent has children
+ bool hasChildren = false;
+ for (const QVariant &v : agents) {
+ if (v.toMap().value("parentAgentId").toString() == agentId) {
+ hasChildren = true;
+ break;
+ }
+ }
+
+ // Track expansion for agents with children
+ bool expanded = true;
+ if (hasChildren) {
+ expanded = m_agentExpanded.value(agentId, true);
+ m_agentExpanded[agentId] = expanded;
+ }
+
Row child;
- child.type = "agent"; child.parentId = id; child.agentId = agent.value("id").toString();
- child.level = 1; child.parentExpanded = expanded;
+ child.type = "agent";
+ child.parentId = sessionId;
+ child.agentId = agentId;
+ child.parentAgentId = parentAgentId;
+ child.level = level;
+ child.parentExpanded = parentExpanded;
+ child.expanded = expanded;
+ child.hasChildren = hasChildren;
child.state = agent.value("state").toString();
if (child.state.isEmpty()) child.state = "idle";
- child.displayName = agent.value("name").toString(); child.paused = paused;
+ child.displayName = agent.value("name").toString();
+ child.paused = paused;
m_rows.append(child);
+
+ // Recursively add children of this agent (only if expanded and parent is expanded)
+ appendAgentTree(sessionId, agents, agentId, level + 1, parentExpanded && expanded, paused);
}
}
@@ -121,10 +163,18 @@ void SessionModel::updateVisibility()
void SessionModel::setExpanded(int index, bool expanded)
{
- if (index < 0 || index >= m_rows.size() || m_rows[index].type != "session") return;
- m_rows[index].expanded = expanded;
- m_expanded[m_rows[index].id] = expanded;
- updateVisibility();
+ if (index < 0 || index >= m_rows.size()) return;
+ const Row &row = m_rows[index];
+ if (row.type == "session") {
+ m_rows[index].expanded = expanded;
+ m_expanded[row.id] = expanded;
+ } else if (row.type == "agent" && row.hasChildren) {
+ m_rows[index].expanded = expanded;
+ m_agentExpanded[row.agentId] = expanded;
+ } else {
+ return;
+ }
+ rebuild(); // Rebuild to update visibility
}
QVariantMap SessionModel::get(int index) const
@@ -133,11 +183,13 @@ QVariantMap SessionModel::get(int index) const
if (index < 0 || index >= m_rows.size()) return result;
const Row &r = m_rows.at(index);
result["type"] = r.type; result["id"] = r.id; result["parentId"] = r.parentId;
- result["agentId"] = r.agentId; result["level"] = r.level; result["expanded"] = r.expanded;
- result["parentExpanded"] = r.parentExpanded; result["state"] = r.state;
+ result["agentId"] = r.agentId; result["parentAgentId"] = r.parentAgentId;
+ result["level"] = r.level; result["expanded"] = r.expanded;
+ result["parentExpanded"] = r.parentExpanded; result["hasChildren"] = r.hasChildren;
+ result["state"] = r.state;
result["model"] = r.model; result["displayName"] = r.displayName;
result["baseName"] = r.baseName; result["connectionColor"] = r.connectionColor;
- result["paused"] = r.paused;
+ result["paused"] = r.paused; result["name"] = r.displayName;
return result;
}
diff --git a/kde/gui/sessionmodel.h b/kde/gui/sessionmodel.h
index 374168c..2854977 100644
--- a/kde/gui/sessionmodel.h
+++ b/kde/gui/sessionmodel.h
@@ -22,6 +22,7 @@ public:
LevelRole,
ExpandedRole,
ParentExpandedRole,
+ HasChildrenRole,
StateRole,
ModelRole,
DisplayNameRole,
@@ -51,11 +52,13 @@ private:
struct Row {
QString type;
QString id;
- QString parentId;
+ QString parentId; // session ID for agents
QString agentId;
+ QString parentAgentId; // parent agent ID for sub-agents
int level = 0;
bool expanded = true;
bool parentExpanded = true;
+ bool hasChildren = false;
QString state;
QString model;
QString displayName;
@@ -66,11 +69,14 @@ private:
static QString connectionColor(const QVariantMap &session);
void appendSession(const QVariantMap &session);
+ void appendAgentTree(const QString &sessionId, const QVariantList &agents,
+ const QString &parentAgentId, int level, bool parentExpanded, bool paused);
void updateVisibility();
Ollie9pClient *m_client;
QList m_rows;
- QHash m_expanded;
+ QHash m_expanded; // session expansion
+ QHash m_agentExpanded; // agent expansion
};
#endif
diff --git a/kde/krunner/ollie_runner.cpp b/kde/krunner/ollie_runner.cpp
index 8cd5ec7..ac43120 100644
--- a/kde/krunner/ollie_runner.cpp
+++ b/kde/krunner/ollie_runner.cpp
@@ -98,16 +98,24 @@ void OllieRunner::match(RunnerNS::RunnerContext &context)
} else {
for (const QString &entry : sessions) {
QStringList parts = entry.split('\t');
+ // New format: session-id\tsession-name\tpaused\tconnected\tremote\tcwd
if (parts.size() < 4) continue;
+ QString sessionId = parts[0];
+ QString sessionName = parts[1];
+ bool paused = (parts[2] == "true");
+ bool connected = (parts[3] == "true");
+ QString cwd = parts.value(5);
+
RunnerNS::QueryMatch match(this);
- match.setId("ollie-session-" + parts[0]);
- QString state = parts[1] == "idle" ? "○" : "●";
- match.setText(state + " " + parts[0].left(14) + " [" + parts[3] + "]");
- match.setSubtext(parts[2] + " — " + parts[1]);
+ match.setId("ollie-session-" + sessionId);
+ QString state = connected ? "●" : "○";
+ if (paused) state = "◐";
+ match.setText(state + " " + sessionName);
+ match.setSubtext(cwd);
match.setIconName("system-run");
match.setRelevance(0.7);
- match.setData(parts[0]);
+ match.setData(sessionId);
context.addMatch(match);
}
}