Split session/idx and agent/idx, fix sub-agent tree display
Server:
- session/idx now outputs: session-id, session-name, paused, connected, remote, cwd
- session/{s}/agent/idx now outputs: session-id, agent-id, agent-name, parent-id, depth, state
- Add error if parent agent not found during sub-agent spawn
GUI:
- refreshSessions() reads session index, then agent/idx per session
- Auto-select first top-level agent (depth 0) instead of first in list
- SessionModel tracks agent expansion separately from session expansion
- Agents with children show expand/collapse arrows and are collapsible
- Add hasChildren role to SessionModel
Tools:
- subagent_spawn now passes parent= argument for proper depth tracking
- Remove hardcoded max_depth=5, use server default of 1
- Makefile installs shell script tools from data/tools/
KRunner:
- Update to parse new session/idx format
This commit is contained in:
parent
958d3d2ddf
commit
a17dd683df
2
Makefile
2
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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<file>SettingsDialog.qml</file>
|
||||
<file>NewSessionDialog.qml</file>
|
||||
<file>NewAgentDialog.qml</file>
|
||||
<file>GoalDialog.qml</file>
|
||||
<file>ChatBlockDelegate.qml</file>
|
||||
<file>SessionTree.qml</file>
|
||||
<file>ChatPane.qml</file>
|
||||
|
|
|
|||
|
|
@ -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<int, QByteArray> 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Row> m_rows;
|
||||
QHash<QString, bool> m_expanded;
|
||||
QHash<QString, bool> m_expanded; // session expansion
|
||||
QHash<QString, bool> m_agentExpanded; // agent expansion
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue