gui: add pending bypass indicator to agent tree

Track multiple concurrent pending bypasses per agent in C++ backend:
- Add m_pendingBypasses QHash<QString,QSet<QString>> 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.
This commit is contained in:
Levi Neely 2026-10-06 14:47:55 +02:00
parent f9e6785add
commit 150ca5dfdd
4 changed files with 52 additions and 4 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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();

View File

@ -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<QString, QString> m_agentStateValues;
// Pending bypass requests per agent: agentKey -> set of bypass IDs
QHash<QString, QSet<QString>> m_pendingBypasses;
// Native streaming readers
Lib9pStreamer *m_chat = nullptr; // Guarded — reads agent chat log