From 150ca5dfdde2301ebdceaf12f226dca4c14257ad Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Tue, 6 Oct 2026 14:47:55 +0200 Subject: [PATCH] gui: add pending bypass indicator to agent tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track multiple concurrent pending bypasses per agent in C++ backend: - Add m_pendingBypasses QHash> keyed by session:agent - Add pendingBypassCount(sessionId, agentId) Q_INVOKABLE - Add pendingBypassCountChanged(sessionId, agentId) signal - Update resolveBypass to take agentId and remove from tracking set SessionTree.qml: show ⚠ indicator left of agent name when the agent has pending bypass requests. Uses Connections to refresh on signal. ChatPane.qml: pass agentId to resolveBypass calls. --- kde/gui/ChatPane.qml | 4 ++-- kde/gui/SessionTree.qml | 20 ++++++++++++++++++++ kde/gui/ollie9pclient.cpp | 24 +++++++++++++++++++++++- kde/gui/ollie9pclient.h | 8 +++++++- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/kde/gui/ChatPane.qml b/kde/gui/ChatPane.qml index 629480e..b9f393e 100644 --- a/kde/gui/ChatPane.qml +++ b/kde/gui/ChatPane.qml @@ -188,7 +188,7 @@ Item { highlighted: true onClicked: { if (chatPane.pendingBypass) { - ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.id, true) + ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, true) chatPane.pendingBypass = null chatPane.hasPendingBypass = false } @@ -199,7 +199,7 @@ Item { text: "Deny" onClicked: { if (chatPane.pendingBypass) { - ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.id, false) + ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, false) chatPane.pendingBypass = null chatPane.hasPendingBypass = false } diff --git a/kde/gui/SessionTree.qml b/kde/gui/SessionTree.qml index fb70e41..b41536f 100644 --- a/kde/gui/SessionTree.qml +++ b/kde/gui/SessionTree.qml @@ -229,6 +229,26 @@ Item { visible: type === "session" } + // Pending bypass indicator (agent rows only) + Label { + id: bypassIndicator + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: 4 + text: "⚠" + color: "#e6a700" + font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12 + visible: type === "agent" && ollie.pendingBypassCount(parentId || "", agentId || "") > 0 + + Connections { + target: ollie + function onPendingBypassCountChanged(sid, aid) { + if (type === "agent" && sid === parentId && aid === agentId) { + bypassIndicator.visible = ollie.pendingBypassCount(parentId || "", agentId || "") > 0 + } + } + } + } + // Name area Item { Layout.fillWidth: true diff --git a/kde/gui/ollie9pclient.cpp b/kde/gui/ollie9pclient.cpp index 9424996..9ecea1a 100644 --- a/kde/gui/ollie9pclient.cpp +++ b/kde/gui/ollie9pclient.cpp @@ -243,6 +243,12 @@ void Ollie9pClient::handleEvent(const QString &eventLine) QString cmd = payload.mid(tab1 + 1, tab2 - tab1 - 1).toString(); cmd.replace(QLatin1String("\\n"), QLatin1String("\n")); // unescape newlines const QString cwd = payload.mid(tab2 + 1).toString(); + + // Track pending bypass + const QString key = agentKey(sessionId.toString(), agentId.toString()); + m_pendingBypasses[key].insert(id); + emit pendingBypassCountChanged(sessionId.toString(), agentId.toString()); + emit bypassRequested(sessionId.toString(), agentId.toString(), id, cmd, cwd); } } else { @@ -260,6 +266,12 @@ QString Ollie9pClient::agentState(const QString &sessionId, const QString &agent return m_agentStateValues.value(agentKey(sessionId, agentId), "idle"); } +int Ollie9pClient::pendingBypassCount(const QString &sessionId, const QString &agentId) const +{ + const QString key = agentKey(sessionId, agentId); + return m_pendingBypasses.value(key).size(); +} + void Ollie9pClient::refreshSessions() { if (!m_daemonConnected) return; @@ -764,7 +776,7 @@ bool Ollie9pClient::renameAgent(const QString &sessionId, const QString &agentId return false; } -bool Ollie9pClient::resolveBypass(const QString &sessionId, const QString &id, bool approve) +bool Ollie9pClient::resolveBypass(const QString &sessionId, const QString &agentId, const QString &id, bool approve) { if (sessionId.isEmpty() || id.isEmpty()) return false; if (!m_9p || !m_9p->isConnected()) return false; @@ -772,6 +784,16 @@ bool Ollie9pClient::resolveBypass(const QString &sessionId, const QString &id, b QString path = "session/" + sessionId + "/bypass"; QString data = id + (approve ? " approve" : " deny"); if (m_9p->write(path, data.toUtf8())) { + // Remove from pending bypasses + const QString key = agentKey(sessionId, agentId); + if (m_pendingBypasses.contains(key)) { + m_pendingBypasses[key].remove(id); + if (m_pendingBypasses[key].isEmpty()) { + m_pendingBypasses.remove(key); + } + emit pendingBypassCountChanged(sessionId, agentId); + emit bypassResolved(sessionId, agentId); + } return true; } qDebug() << "resolveBypass failed:" << m_9p->lastError(); diff --git a/kde/gui/ollie9pclient.h b/kde/gui/ollie9pclient.h index 3ca14fb..824baa6 100644 --- a/kde/gui/ollie9pclient.h +++ b/kde/gui/ollie9pclient.h @@ -54,6 +54,7 @@ public: bool daemonConnected() const { return m_daemonConnected; } Q_INVOKABLE QString agentState(const QString &sessionId, const QString &agentId) const; Q_INVOKABLE QString sessionConnectionColor(const QString &sessionId) const; + Q_INVOKABLE int pendingBypassCount(const QString &sessionId, const QString &agentId) const; Q_INVOKABLE void setCurrentBackend(const QString &backend) { if (m_currentBackend == backend) return; m_currentBackend = backend; @@ -90,7 +91,7 @@ public: Q_INVOKABLE bool killAgent(const QString &sessionId, const QString &agentId); Q_INVOKABLE bool renameSession(const QString &sessionId, const QString &newName); Q_INVOKABLE bool renameAgent(const QString &sessionId, const QString &agentId, const QString &newName); - Q_INVOKABLE bool resolveBypass(const QString &sessionId, const QString &id, bool approve); + Q_INVOKABLE bool resolveBypass(const QString &sessionId, const QString &agentId, const QString &id, bool approve); Q_INVOKABLE QString lastError() const { return m_9p ? m_9p->lastError() : QString(); } signals: @@ -101,6 +102,8 @@ signals: void agentStateChanged(const QString &sessionId, const QString &agentId, const QString &state); void chatReceived(const QString &text); void bypassRequested(const QString &sessionId, const QString &agentId, const QString &id, const QString &cmd, const QString &cwd); + void bypassResolved(const QString &sessionId, const QString &agentId); + void pendingBypassCountChanged(const QString &sessionId, const QString &agentId); void rootBackendsLoadedChanged(); void availableBackendsChanged(); void rootAgentsLoadedChanged(); @@ -146,6 +149,9 @@ private: // Agent state cache — updated via delta events QHash m_agentStateValues; + + // Pending bypass requests per agent: agentKey -> set of bypass IDs + QHash> m_pendingBypasses; // Native streaming readers Lib9pStreamer *m_chat = nullptr; // Guarded — reads agent chat log