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; \
|
go build -o $(CONFIG_DIR)/tools/$$name ./$$d; \
|
||||||
done
|
done
|
||||||
install -m644 data/tools/*.meta $(CONFIG_DIR)/tools/
|
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:
|
kde:
|
||||||
cmake -S kde -B kde/build -DCMAKE_INSTALL_PREFIX=$(PREFIX)
|
cmake -S kde -B kde/build -DCMAKE_INSTALL_PREFIX=$(PREFIX)
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,9 @@ func buildSessionChildren(
|
||||||
var parent *agent.Agent
|
var parent *agent.Agent
|
||||||
if req.ParentID != "" {
|
if req.ParentID != "" {
|
||||||
parent = s.FindAgent(req.ParentID)
|
parent = s.FindAgent(req.ParentID)
|
||||||
|
if parent == nil {
|
||||||
|
return nil, fmt.Errorf("parent agent %q not found", req.ParentID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if parent != nil {
|
if parent != nil {
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,7 @@ func waitIdle(poll, timeout time.Duration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildIndex builds the session index content.
|
// BuildIndex builds the session index content.
|
||||||
|
// Format: session-id\tsession-name\tpaused\tconnected\tremote\tcwd
|
||||||
func BuildIndex() []byte {
|
func BuildIndex() []byte {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
|
|
@ -265,31 +266,19 @@ func BuildIndex() []byte {
|
||||||
sess.mu.RLock()
|
sess.mu.RLock()
|
||||||
connected := sess.toolsConn != nil && !sess.paused
|
connected := sess.toolsConn != nil && !sess.paused
|
||||||
sess.mu.RUnlock()
|
sess.mu.RUnlock()
|
||||||
var cwd, be, model string
|
fmt.Fprintf(&sb, "%s\t%s\t%t\t%t\t%s\t%s\n",
|
||||||
var agentList []string
|
sess.ID, e.name, e.paused, connected, sess.Remote, sess.Cwd())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return []byte(sb.String())
|
return []byte(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildAgentIndex builds the agent index for a session.
|
// BuildAgentIndex builds the agent index for a session.
|
||||||
|
// Format: session-id\tagent-id\tagent-name\tparent-id\tdepth\tstate
|
||||||
func BuildAgentIndex(sess *Session) []byte {
|
func BuildAgentIndex(sess *Session) []byte {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, ag := range sess.Agents() {
|
for _, ag := range sess.Agents() {
|
||||||
fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\n",
|
fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%d\t%s\n",
|
||||||
ag.Name(), ag.ID(), ag.Cwd(), ag.BackendName(), ag.ModelName())
|
sess.ID, ag.ID(), ag.Name(), ag.ParentID(), ag.Depth(), ag.State())
|
||||||
}
|
}
|
||||||
return []byte(sb.String())
|
return []byte(sb.String())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/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
|
# ollie:prompt
|
||||||
# ## subagent_spawn
|
# ## subagent_spawn
|
||||||
#
|
#
|
||||||
|
|
@ -25,6 +25,7 @@ profile=$(arg_get "profile" "default")
|
||||||
backend=$(arg_get "backend" "")
|
backend=$(arg_get "backend" "")
|
||||||
model=$(arg_get "model" "")
|
model=$(arg_get "model" "")
|
||||||
cwd=$(arg_get "cwd" "$(pwd)")
|
cwd=$(arg_get "cwd" "$(pwd)")
|
||||||
|
parent=$(arg_get "parent" "${OLLIE_UNAME:-}")
|
||||||
|
|
||||||
# Escape newlines in prompt for the key=value wire format.
|
# 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$//')
|
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.
|
# Build the payload.
|
||||||
payload="cwd=${cwd}
|
payload="cwd=${cwd}
|
||||||
profile=${profile}
|
profile=${profile}
|
||||||
parent=${OLLIE_UNAME}
|
|
||||||
timeout=600
|
timeout=600
|
||||||
prompt=${escaped_prompt}"
|
prompt=${escaped_prompt}"
|
||||||
|
|
||||||
|
[ -n "$parent" ] && payload="${payload}
|
||||||
|
parent=${parent}"
|
||||||
[ -n "$name" ] && payload="${payload}
|
[ -n "$name" ] && payload="${payload}
|
||||||
name=${name}"
|
name=${name}"
|
||||||
[ -n "$backend" ] && payload="${payload}
|
[ -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 int renamingIndex: -1
|
||||||
property string renamingText: ""
|
property string renamingText: ""
|
||||||
property string killTargetSessionId: ""
|
property string killTargetSessionId: ""
|
||||||
|
property string killTargetSessionName: ""
|
||||||
property string killTargetAgentSessionId: ""
|
property string killTargetAgentSessionId: ""
|
||||||
property string killTargetAgentId: ""
|
property string killTargetAgentId: ""
|
||||||
signal newSessionRequested()
|
signal newSessionRequested()
|
||||||
signal settingsRequested()
|
signal settingsRequested()
|
||||||
signal newAgentRequested(string sessionId)
|
signal newAgentRequested(string sessionId)
|
||||||
|
signal goalRequested(string sessionId, string sessionName)
|
||||||
signal renameFailed(string itemType, string attemptedName)
|
signal renameFailed(string itemType, string attemptedName)
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
@ -94,8 +96,10 @@ Item {
|
||||||
if (mouse.button === Qt.RightButton) {
|
if (mouse.button === Qt.RightButton) {
|
||||||
if (type === "session") {
|
if (type === "session") {
|
||||||
var item = sessionModel.get(parent.modelIndex)
|
var item = sessionModel.get(parent.modelIndex)
|
||||||
if (item)
|
if (item) {
|
||||||
tree.killTargetSessionId = item.id
|
tree.killTargetSessionId = item.id
|
||||||
|
tree.killTargetSessionName = item.name
|
||||||
|
}
|
||||||
sessionCtxMenu.popup()
|
sessionCtxMenu.popup()
|
||||||
} else if (type === "agent") {
|
} else if (type === "agent") {
|
||||||
var agentItem = sessionModel.get(parent.modelIndex)
|
var agentItem = sessionModel.get(parent.modelIndex)
|
||||||
|
|
@ -128,6 +132,10 @@ Item {
|
||||||
sessionModel.setExpanded(parent.modelIndex, !expanded)
|
sessionModel.setExpanded(parent.modelIndex, !expanded)
|
||||||
} else if (type === "agent") {
|
} else if (type === "agent") {
|
||||||
ollie.switchAgent(parentId, agentId)
|
ollie.switchAgent(parentId, agentId)
|
||||||
|
// Toggle expansion if agent has children
|
||||||
|
if (hasChildren) {
|
||||||
|
sessionModel.setExpanded(parent.modelIndex, !expanded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onDoubleClicked: {
|
onDoubleClicked: {
|
||||||
|
|
@ -138,6 +146,12 @@ Item {
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: sessionCtxMenu
|
id: sessionCtxMenu
|
||||||
|
MenuItem {
|
||||||
|
text: "Goal..."
|
||||||
|
onClicked: {
|
||||||
|
tree.goalRequested(tree.killTargetSessionId, tree.killTargetSessionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
MenuItem {
|
MenuItem {
|
||||||
text: "New Agent..."
|
text: "New Agent..."
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
@ -189,7 +203,7 @@ Item {
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: type === "session" ? (expanded ? "▼" : "▶") : ""
|
text: (type === "session" || hasChildren) ? (expanded ? "▼" : "▶") : ""
|
||||||
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
|
||||||
color: theme.textColor
|
color: theme.textColor
|
||||||
opacity: 0.6
|
opacity: 0.6
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,11 @@ ApplicationWindow {
|
||||||
targetSessionId: ""
|
targetSessionId: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Goal Dialog ---
|
||||||
|
GoalDialog {
|
||||||
|
id: goalDialog
|
||||||
|
}
|
||||||
|
|
||||||
SplitView {
|
SplitView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
|
@ -178,6 +183,11 @@ ApplicationWindow {
|
||||||
newAgentDialog.targetSessionId = sessionId
|
newAgentDialog.targetSessionId = sessionId
|
||||||
newAgentDialog.open()
|
newAgentDialog.open()
|
||||||
}
|
}
|
||||||
|
onGoalRequested: function(sessionId, sessionName) {
|
||||||
|
goalDialog.sessionId = sessionId
|
||||||
|
goalDialog.sessionName = sessionName
|
||||||
|
goalDialog.open()
|
||||||
|
}
|
||||||
onRenameFailed: function(itemType, attemptedName) {
|
onRenameFailed: function(itemType, attemptedName) {
|
||||||
renameErrorDialog.itemType = itemType
|
renameErrorDialog.itemType = itemType
|
||||||
renameErrorDialog.attemptedName = attemptedName
|
renameErrorDialog.attemptedName = attemptedName
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,7 @@ void Ollie9pClient::refreshSessions()
|
||||||
if (!m_daemonConnected) return;
|
if (!m_daemonConnected) return;
|
||||||
if (!m_9p || !m_9p->isConnected()) 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"));
|
QByteArray out = m_9p->read(QStringLiteral("session/idx"));
|
||||||
QString raw = QString::fromUtf8(out);
|
QString raw = QString::fromUtf8(out);
|
||||||
|
|
||||||
|
|
@ -275,12 +276,14 @@ void Ollie9pClient::refreshSessions()
|
||||||
if (!raw.isEmpty()) {
|
if (!raw.isEmpty()) {
|
||||||
for (const QString &line : raw.split('\n', Qt::SkipEmptyParts)) {
|
for (const QString &line : raw.split('\n', Qt::SkipEmptyParts)) {
|
||||||
QStringList parts = line.split('\t');
|
QStringList parts = line.split('\t');
|
||||||
// New format: name\tid\tpaused\tconnected\tcwd\tbackend\tmodel\tagents
|
if (parts.size() < 4) continue;
|
||||||
// Where agents is semicolon-separated: name:id:state;name:id:state
|
|
||||||
if (parts.size() < 2) 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 bool isConnected = (parts.value(3) == "true");
|
||||||
|
const QString remote = parts.value(4);
|
||||||
|
const QString cwd = parts.value(5);
|
||||||
const bool isActiveSession = (sessionId == m_activeSessionId);
|
const bool isActiveSession = (sessionId == m_activeSessionId);
|
||||||
|
|
||||||
if (isActiveSession) {
|
if (isActiveSession) {
|
||||||
|
|
@ -288,26 +291,30 @@ void Ollie9pClient::refreshSessions()
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap session;
|
QVariantMap session;
|
||||||
session["name"] = parts.value(0);
|
|
||||||
session["id"] = sessionId;
|
session["id"] = sessionId;
|
||||||
session["paused"] = (parts.value(2) == "true");
|
session["name"] = sessionName;
|
||||||
|
session["paused"] = isPaused;
|
||||||
session["connected"] = isConnected;
|
session["connected"] = isConnected;
|
||||||
session["cwd"] = parts.value(4);
|
session["remote"] = remote;
|
||||||
session["backend"] = parts.value(5);
|
session["cwd"] = cwd;
|
||||||
session["model"] = parts.value(6);
|
|
||||||
|
|
||||||
// Parse agents from semicolon-separated list
|
// Read agent index for this session: session-id\tagent-id\tagent-name\tparent-id\tdepth\tstate
|
||||||
QVariantList agents;
|
QVariantList agents;
|
||||||
QString agentStr = parts.value(7);
|
QString agentIdxPath = "session/" + sessionId + "/agent/idx";
|
||||||
if (!agentStr.isEmpty()) {
|
QByteArray agentOut = m_9p->read(agentIdxPath);
|
||||||
for (const QString &agentEntry : agentStr.split(';', Qt::SkipEmptyParts)) {
|
QString agentRaw = QString::fromUtf8(agentOut);
|
||||||
QStringList agentParts = agentEntry.split(':');
|
if (!agentRaw.isEmpty()) {
|
||||||
if (agentParts.size() >= 3) {
|
for (const QString &agentLine : agentRaw.split('\n', Qt::SkipEmptyParts)) {
|
||||||
|
QStringList agentParts = agentLine.split('\t');
|
||||||
|
if (agentParts.size() < 5) continue;
|
||||||
|
|
||||||
const QString agentId = agentParts.value(1);
|
const QString agentId = agentParts.value(1);
|
||||||
QVariantMap av;
|
QVariantMap av;
|
||||||
av["name"] = agentParts.value(0);
|
|
||||||
av["id"] = agentId;
|
av["id"] = agentId;
|
||||||
av["state"] = agentParts.value(2);
|
av["name"] = agentParts.value(2);
|
||||||
|
av["parentAgentId"] = agentParts.value(3);
|
||||||
|
av["depth"] = agentParts.value(4).toInt();
|
||||||
|
av["state"] = agentParts.value(5);
|
||||||
agents.append(av);
|
agents.append(av);
|
||||||
|
|
||||||
// Check if this is the active agent
|
// Check if this is the active agent
|
||||||
|
|
@ -316,7 +323,6 @@ void Ollie9pClient::refreshSessions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
session["agents"] = agents;
|
session["agents"] = agents;
|
||||||
m_sessions.append(session);
|
m_sessions.append(session);
|
||||||
}
|
}
|
||||||
|
|
@ -354,8 +360,17 @@ void Ollie9pClient::refreshSessions()
|
||||||
if (session.value("id").toString() != m_activeSessionId) continue;
|
if (session.value("id").toString() != m_activeSessionId) continue;
|
||||||
const QVariantList agents = session.value("agents").toList();
|
const QVariantList agents = session.value("agents").toList();
|
||||||
if (!agents.isEmpty()) {
|
if (!agents.isEmpty()) {
|
||||||
const QString aid = agents.first().toMap().value("id").toString();
|
// Select first top-level agent (depth 0)
|
||||||
if (!aid.isEmpty()) switchAgent(m_activeSessionId, aid);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -367,9 +382,17 @@ void Ollie9pClient::refreshSessions()
|
||||||
const QString sid = firstSession.value("id").toString();
|
const QString sid = firstSession.value("id").toString();
|
||||||
const QVariantList agents = firstSession.value("agents").toList();
|
const QVariantList agents = firstSession.value("agents").toList();
|
||||||
if (!agents.isEmpty()) {
|
if (!agents.isEmpty()) {
|
||||||
// Select first agent of first session
|
// Select first top-level agent (depth 0)
|
||||||
const QString aid = agents.first().toMap().value("id").toString();
|
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);
|
switchAgent(sid, aid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// No agents - just select the session
|
// No agents - just select the session
|
||||||
setActiveSessionId(sid);
|
setActiveSessionId(sid);
|
||||||
|
|
@ -603,6 +626,15 @@ QString Ollie9pClient::createSession(const QString &name, const QString &remote,
|
||||||
return QString(); // success
|
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)
|
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");
|
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 QStringList getAvailableModels(const QString &backend) const;
|
||||||
Q_INVOKABLE bool createQuickSession();
|
Q_INVOKABLE bool createQuickSession();
|
||||||
Q_INVOKABLE QString createSession(const QString &name, const QString &remote, const QString &workflow = QString());
|
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 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 killAgent(const QString &sessionId, const QString &agentId);
|
||||||
Q_INVOKABLE bool renameSession(const QString &sessionId, const QString &newName);
|
Q_INVOKABLE bool renameSession(const QString &sessionId, const QString &newName);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<file>SettingsDialog.qml</file>
|
<file>SettingsDialog.qml</file>
|
||||||
<file>NewSessionDialog.qml</file>
|
<file>NewSessionDialog.qml</file>
|
||||||
<file>NewAgentDialog.qml</file>
|
<file>NewAgentDialog.qml</file>
|
||||||
|
<file>GoalDialog.qml</file>
|
||||||
<file>ChatBlockDelegate.qml</file>
|
<file>ChatBlockDelegate.qml</file>
|
||||||
<file>SessionTree.qml</file>
|
<file>SessionTree.qml</file>
|
||||||
<file>ChatPane.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 LevelRole: return r.level;
|
||||||
case ExpandedRole: return r.expanded;
|
case ExpandedRole: return r.expanded;
|
||||||
case ParentExpandedRole: return r.parentExpanded;
|
case ParentExpandedRole: return r.parentExpanded;
|
||||||
|
case HasChildrenRole: return r.hasChildren;
|
||||||
case StateRole: return r.state;
|
case StateRole: return r.state;
|
||||||
case ModelRole: return r.model;
|
case ModelRole: return r.model;
|
||||||
case DisplayNameRole: return r.displayName;
|
case DisplayNameRole: return r.displayName;
|
||||||
|
|
@ -40,7 +41,8 @@ QHash<int, QByteArray> SessionModel::roleNames() const
|
||||||
{
|
{
|
||||||
return {{TypeRole, "type"}, {IdRole, "id"}, {ParentIdRole, "parentId"},
|
return {{TypeRole, "type"}, {IdRole, "id"}, {ParentIdRole, "parentId"},
|
||||||
{AgentIdRole, "agentId"}, {LevelRole, "level"}, {ExpandedRole, "expanded"},
|
{AgentIdRole, "agentId"}, {LevelRole, "level"}, {ExpandedRole, "expanded"},
|
||||||
{ParentExpandedRole, "parentExpanded"}, {StateRole, "state"}, {ModelRole, "model"},
|
{ParentExpandedRole, "parentExpanded"}, {HasChildrenRole, "hasChildren"},
|
||||||
|
{StateRole, "state"}, {ModelRole, "model"},
|
||||||
{DisplayNameRole, "displayName"}, {BaseNameRole, "baseName"},
|
{DisplayNameRole, "displayName"}, {BaseNameRole, "baseName"},
|
||||||
{ConnectionColorRole, "connectionColor"}, {PausedRole, "paused"}};
|
{ConnectionColorRole, "connectionColor"}, {PausedRole, "paused"}};
|
||||||
}
|
}
|
||||||
|
|
@ -65,21 +67,61 @@ void SessionModel::appendSession(const QVariantMap &session)
|
||||||
|
|
||||||
Row row;
|
Row row;
|
||||||
row.type = "session"; row.id = id; row.expanded = expanded; row.paused = paused;
|
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.baseName = name;
|
||||||
row.displayName = agents.isEmpty() ? name : name + " (" + QString::number(agents.size()) + ")";
|
row.displayName = agents.isEmpty() ? name : name + " (" + QString::number(agents.size()) + ")";
|
||||||
row.connectionColor = connectionColor(session);
|
row.connectionColor = connectionColor(session);
|
||||||
m_rows.append(row);
|
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) {
|
for (const QVariant &value : agents) {
|
||||||
const QVariantMap agent = value.toMap();
|
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;
|
Row child;
|
||||||
child.type = "agent"; child.parentId = id; child.agentId = agent.value("id").toString();
|
child.type = "agent";
|
||||||
child.level = 1; child.parentExpanded = expanded;
|
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();
|
child.state = agent.value("state").toString();
|
||||||
if (child.state.isEmpty()) child.state = "idle";
|
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);
|
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)
|
void SessionModel::setExpanded(int index, bool expanded)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= m_rows.size() || m_rows[index].type != "session") return;
|
if (index < 0 || index >= m_rows.size()) return;
|
||||||
|
const Row &row = m_rows[index];
|
||||||
|
if (row.type == "session") {
|
||||||
m_rows[index].expanded = expanded;
|
m_rows[index].expanded = expanded;
|
||||||
m_expanded[m_rows[index].id] = expanded;
|
m_expanded[row.id] = expanded;
|
||||||
updateVisibility();
|
} 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
|
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;
|
if (index < 0 || index >= m_rows.size()) return result;
|
||||||
const Row &r = m_rows.at(index);
|
const Row &r = m_rows.at(index);
|
||||||
result["type"] = r.type; result["id"] = r.id; result["parentId"] = r.parentId;
|
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["agentId"] = r.agentId; result["parentAgentId"] = r.parentAgentId;
|
||||||
result["parentExpanded"] = r.parentExpanded; result["state"] = r.state;
|
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["model"] = r.model; result["displayName"] = r.displayName;
|
||||||
result["baseName"] = r.baseName; result["connectionColor"] = r.connectionColor;
|
result["baseName"] = r.baseName; result["connectionColor"] = r.connectionColor;
|
||||||
result["paused"] = r.paused;
|
result["paused"] = r.paused; result["name"] = r.displayName;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ public:
|
||||||
LevelRole,
|
LevelRole,
|
||||||
ExpandedRole,
|
ExpandedRole,
|
||||||
ParentExpandedRole,
|
ParentExpandedRole,
|
||||||
|
HasChildrenRole,
|
||||||
StateRole,
|
StateRole,
|
||||||
ModelRole,
|
ModelRole,
|
||||||
DisplayNameRole,
|
DisplayNameRole,
|
||||||
|
|
@ -51,11 +52,13 @@ private:
|
||||||
struct Row {
|
struct Row {
|
||||||
QString type;
|
QString type;
|
||||||
QString id;
|
QString id;
|
||||||
QString parentId;
|
QString parentId; // session ID for agents
|
||||||
QString agentId;
|
QString agentId;
|
||||||
|
QString parentAgentId; // parent agent ID for sub-agents
|
||||||
int level = 0;
|
int level = 0;
|
||||||
bool expanded = true;
|
bool expanded = true;
|
||||||
bool parentExpanded = true;
|
bool parentExpanded = true;
|
||||||
|
bool hasChildren = false;
|
||||||
QString state;
|
QString state;
|
||||||
QString model;
|
QString model;
|
||||||
QString displayName;
|
QString displayName;
|
||||||
|
|
@ -66,11 +69,14 @@ private:
|
||||||
|
|
||||||
static QString connectionColor(const QVariantMap &session);
|
static QString connectionColor(const QVariantMap &session);
|
||||||
void appendSession(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();
|
void updateVisibility();
|
||||||
|
|
||||||
Ollie9pClient *m_client;
|
Ollie9pClient *m_client;
|
||||||
QList<Row> m_rows;
|
QList<Row> m_rows;
|
||||||
QHash<QString, bool> m_expanded;
|
QHash<QString, bool> m_expanded; // session expansion
|
||||||
|
QHash<QString, bool> m_agentExpanded; // agent expansion
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -98,16 +98,24 @@ void OllieRunner::match(RunnerNS::RunnerContext &context)
|
||||||
} else {
|
} else {
|
||||||
for (const QString &entry : sessions) {
|
for (const QString &entry : sessions) {
|
||||||
QStringList parts = entry.split('\t');
|
QStringList parts = entry.split('\t');
|
||||||
|
// New format: session-id\tsession-name\tpaused\tconnected\tremote\tcwd
|
||||||
if (parts.size() < 4) continue;
|
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);
|
RunnerNS::QueryMatch match(this);
|
||||||
match.setId("ollie-session-" + parts[0]);
|
match.setId("ollie-session-" + sessionId);
|
||||||
QString state = parts[1] == "idle" ? "○" : "●";
|
QString state = connected ? "●" : "○";
|
||||||
match.setText(state + " " + parts[0].left(14) + " [" + parts[3] + "]");
|
if (paused) state = "◐";
|
||||||
match.setSubtext(parts[2] + " — " + parts[1]);
|
match.setText(state + " " + sessionName);
|
||||||
|
match.setSubtext(cwd);
|
||||||
match.setIconName("system-run");
|
match.setIconName("system-run");
|
||||||
match.setRelevance(0.7);
|
match.setRelevance(0.7);
|
||||||
match.setData(parts[0]);
|
match.setData(sessionId);
|
||||||
context.addMatch(match);
|
context.addMatch(match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue