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); } }