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

View File

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

View File

@ -112,7 +112,7 @@ 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 bypassResolved(const QString &sessionId, const QString &agentId, const QString &requestId);
void pendingBypassCountChanged(const QString &sessionId, const QString &agentId);
void procStarted(const QString &sessionId, const QString &agentId, const QString &payload);
void rootBackendsLoadedChanged();