gui: support multiple concurrent bypass requests per agent

Frontend changes to match the backend's multi-request support:

- bypassResolved signal now includes the request ID
- ChatPane tracks pendingBypasses as a map (id -> request)
- onBypassRequested adds to the map; onBypassResolved removes by ID
- currentBypass shows the first pending request for the active agent
- Switching agents updates the banner to show that agent's pending request
- Approve/Deny now remove only the specific resolved request

This allows each agent to have multiple pending bypass requests without
blocking other agents, and the banner correctly shows requests only for
the currently active agent.
This commit is contained in:
Levi Neely 2026-10-09 10:21:09 +02:00
parent 5c0f463e4d
commit 50ebcedb8a
3 changed files with 53 additions and 52 deletions

View File

@ -7,12 +7,27 @@ Item {
id: chatPane id: chatPane
property bool sidebarVisible: true property bool sidebarVisible: true
property int maxVisibleBlocks: 80 property int maxVisibleBlocks: 80
property var pendingBypass: null // {sessionId, id, cmd, cwd} // Map of pending bypass requests: id -> {sessionId, agentId, id, cmd, cwd}
property bool hasPendingBypass: false property var pendingBypasses: ({})
property var currentBypass: null // The one currently shown in the banner
signal toggleSidebar() signal toggleSidebar()
// Do not opt the chat pane into focus navigation. // Do not opt the chat pane into focus navigation.
activeFocusOnTab: false activeFocusOnTab: false
// Helper to get the first pending bypass for the active agent
function updateCurrentBypass() {
var activeKey = ollie.activeSessionId + ":" + ollie.activeAgentId
for (var id in pendingBypasses) {
var req = pendingBypasses[id]
var key = req.sessionId + ":" + req.agentId
if (key === activeKey) {
currentBypass = req
return
}
}
currentBypass = null
}
// Expose method to set prompt text (for Edit & Resend) // Expose method to set prompt text (for Edit & Resend)
function setPromptText(text) { function setPromptText(text) {
promptInput.text = text promptInput.text = text
@ -175,7 +190,7 @@ Item {
color: "#fff3e0" // amber light color: "#fff3e0" // amber light
border.color: "#ff9800" border.color: "#ff9800"
border.width: 1 border.width: 1
visible: chatPane.hasPendingBypass visible: chatPane.currentBypass !== null
z: 100 z: 100
ColumnLayout { ColumnLayout {
@ -192,7 +207,7 @@ Item {
} }
Label { Label {
text: chatPane.pendingBypass ? chatPane.pendingBypass.cmd : "" text: chatPane.currentBypass ? chatPane.currentBypass.cmd : ""
font.family: "monospace" font.family: "monospace"
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12 font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
wrapMode: Text.WrapAnywhere wrapMode: Text.WrapAnywhere
@ -201,10 +216,10 @@ Item {
} }
Label { Label {
text: chatPane.pendingBypass ? "cwd: " + chatPane.pendingBypass.cwd : "" text: chatPane.currentBypass ? "cwd: " + chatPane.currentBypass.cwd : ""
font.pixelSize: theme.fontSize > 0 ? theme.fontSize - 1 : 10 font.pixelSize: theme.fontSize > 0 ? theme.fontSize - 1 : 10
color: "#666" color: "#666"
visible: chatPane.pendingBypass && chatPane.pendingBypass.cwd !== "" visible: chatPane.currentBypass && chatPane.currentBypass.cwd !== ""
} }
RowLayout { RowLayout {
@ -215,10 +230,8 @@ Item {
text: "Approve" text: "Approve"
highlighted: true highlighted: true
onClicked: { onClicked: {
if (chatPane.pendingBypass) { if (chatPane.currentBypass) {
ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, true) ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, true)
chatPane.pendingBypass = null
chatPane.hasPendingBypass = false
} }
} }
} }
@ -226,10 +239,8 @@ Item {
Button { Button {
text: "Deny" text: "Deny"
onClicked: { onClicked: {
if (chatPane.pendingBypass) { if (chatPane.currentBypass) {
ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, false) ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, false)
chatPane.pendingBypass = null
chatPane.hasPendingBypass = false
} }
} }
} }
@ -580,56 +591,46 @@ Connections {
target: ollie target: ollie
function onActiveSessionIdChanged() { function onActiveSessionIdChanged() {
if (!ollie.activeAgentId) chatPane.loadChat() if (!ollie.activeAgentId) chatPane.loadChat()
// Hide bypass banner if it's not for the now-active session // Update current bypass for the new active agent
if (chatPane.pendingBypass && chatPane.pendingBypass.sessionId !== ollie.activeSessionId) { chatPane.updateCurrentBypass()
chatPane.hasPendingBypass = false
}
} }
function onActiveAgentIdChanged() { function onActiveAgentIdChanged() {
if (ollie.activeAgentId) chatPane.loadChat() if (ollie.activeAgentId) chatPane.loadChat()
// Hide bypass banner if it's not for the now-active agent // Update current bypass for the new active agent
if (chatPane.pendingBypass && chatPane.updateCurrentBypass()
(chatPane.pendingBypass.sessionId !== ollie.activeSessionId ||
chatPane.pendingBypass.agentId !== ollie.activeAgentId)) {
chatPane.hasPendingBypass = false
}
} }
function onBypassRequested(sessionId, agentId, id, cmd, cwd) { function onBypassRequested(sessionId, agentId, id, cmd, cwd) {
// Show bypass request only for the currently active agent // Add to pending bypasses map
if (sessionId === ollie.activeSessionId && agentId === ollie.activeAgentId) { var newMap = chatPane.pendingBypasses
chatPane.pendingBypass = { newMap[id] = {
sessionId: sessionId, sessionId: sessionId,
agentId: agentId, agentId: agentId,
id: id, id: id,
cmd: cmd, cmd: cmd,
cwd: cwd cwd: cwd
} }
chatPane.hasPendingBypass = true chatPane.pendingBypasses = newMap
chatPane.updateCurrentBypass()
} }
} function onBypassResolved(sessionId, agentId, requestId) {
function onBypassResolved(sessionId, agentId) { // Remove the specific resolved request from the map
// Clear bypass banner if it was for this agent (resolved by CLI or another client) if (chatPane.pendingBypasses[requestId]) {
if (chatPane.pendingBypass && var newMap = chatPane.pendingBypasses
chatPane.pendingBypass.sessionId === sessionId && delete newMap[requestId]
chatPane.pendingBypass.agentId === agentId) { chatPane.pendingBypasses = newMap
chatPane.pendingBypass = null chatPane.updateCurrentBypass()
chatPane.hasPendingBypass = false
} }
} }
// Public functions for keyboard shortcuts // Public functions for keyboard shortcuts
function approveBypass() { function approveBypass() {
if (chatPane.pendingBypass) { if (chatPane.currentBypass) {
ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, true) ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, true)
chatPane.pendingBypass = null
chatPane.hasPendingBypass = false
} }
} }
function denyBypass() { function denyBypass() {
if (chatPane.pendingBypass) { if (chatPane.currentBypass) {
ollie.resolveBypass(chatPane.pendingBypass.sessionId, chatPane.pendingBypass.agentId, chatPane.pendingBypass.id, false) ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, false)
chatPane.pendingBypass = null
chatPane.hasPendingBypass = false
} }
} }
} }

View File

@ -280,7 +280,7 @@ void Ollie9pClient::handleEvent(const QString &eventLine)
} }
emit pendingBypassCountChanged(sidStr, aidStr); emit pendingBypassCountChanged(sidStr, aidStr);
} }
emit bypassResolved(sidStr, aidStr); emit bypassResolved(sidStr, aidStr, id);
} }
} }
} else { } else {
@ -895,7 +895,7 @@ bool Ollie9pClient::resolveBypass(const QString &sessionId, const QString &agent
m_pendingBypasses.remove(key); m_pendingBypasses.remove(key);
} }
emit pendingBypassCountChanged(sessionId, agentId); emit pendingBypassCountChanged(sessionId, agentId);
emit bypassResolved(sessionId, agentId); emit bypassResolved(sessionId, agentId, id);
} }
return true; return true;
} }

View File

@ -112,7 +112,7 @@ signals:
void agentStateChanged(const QString &sessionId, const QString &agentId, const QString &state); void agentStateChanged(const QString &sessionId, const QString &agentId, const QString &state);
void chatReceived(const QString &text); void chatReceived(const QString &text);
void bypassRequested(const QString &sessionId, const QString &agentId, const QString &id, const QString &cmd, const QString &cwd); 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 bypassResolved(const QString &sessionId, const QString &agentId, const QString &requestId);
void pendingBypassCountChanged(const QString &sessionId, const QString &agentId); void pendingBypassCountChanged(const QString &sessionId, const QString &agentId);
void procStarted(const QString &sessionId, const QString &agentId, const QString &payload); void procStarted(const QString &sessionId, const QString &agentId, const QString &payload);
void rootBackendsLoadedChanged(); void rootBackendsLoadedChanged();