681 lines
28 KiB
QML
681 lines
28 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
import "TextSelection.js" as TextSelection
|
|
|
|
Item {
|
|
id: chatPane
|
|
property bool sidebarVisible: true
|
|
property int maxVisibleBlocks: 80
|
|
// 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
|
|
promptInput.forceActiveFocus()
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
chatView.positionViewAtEnd()
|
|
}
|
|
function scrollToBlock(blockIndex) {
|
|
if (blockIndex >= 0 && blockIndex < chatModel.count) {
|
|
chatView.positionViewAtIndex(blockIndex, ListView.Center)
|
|
}
|
|
}
|
|
function scrollToBlockId(blockId) {
|
|
var idx = chatModel.indexForBlockId(blockId)
|
|
if (idx >= 0) {
|
|
chatView.positionViewAtIndex(idx, ListView.Center)
|
|
return true
|
|
}
|
|
// Block not in current view — it may have been trimmed.
|
|
// Server-side block IDs are deterministic, so reload full chat.
|
|
if (ollie.activeSessionId && ollie.activeAgentId) {
|
|
chatModel.reset()
|
|
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
|
if (log) {
|
|
chatModel.appendData(log)
|
|
// Don't trim — the bookmark target may be in older history
|
|
idx = chatModel.indexForBlockId(blockId)
|
|
if (idx >= 0) {
|
|
chatView.positionViewAtIndex(idx, ListView.Center)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
console.log("Block not found:", blockId)
|
|
return false
|
|
}
|
|
function trimVisibleBlocks() {
|
|
if (chatModel.count > chatPane.maxVisibleBlocks)
|
|
chatModel.trimFront(chatPane.maxVisibleBlocks)
|
|
}
|
|
function loadChat() {
|
|
chatModel.reset()
|
|
if (ollie.activeSessionId && ollie.activeAgentId) {
|
|
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
|
if (log) {
|
|
chatModel.appendData(log)
|
|
chatModel.trimFront(chatPane.maxVisibleBlocks)
|
|
}
|
|
}
|
|
scrollToBottom()
|
|
}
|
|
|
|
// Get current session's working directory for plumb basedir
|
|
function sessionCwd() {
|
|
if (!ollie.activeSessionId) return ""
|
|
var cfg = ollie.readSessionConfig(ollie.activeSessionId)
|
|
return cfg ? (cfg.cwd || "") : ""
|
|
}
|
|
|
|
// Expand cursor position to surrounding word/path for plumbing
|
|
// Matches acme's isfilec: alphanumeric + . - + / : @
|
|
function wordAtCursor(textEdit, cursorPos) {
|
|
var text = textEdit.getText(0, textEdit.length)
|
|
if (cursorPos < 0 || cursorPos > text.length) return ""
|
|
|
|
function isfilec(ch) {
|
|
return /[a-zA-Z0-9.\-+\/:@]/.test(ch)
|
|
}
|
|
|
|
var start = cursorPos
|
|
while (start > 0 && isfilec(text.charAt(start - 1))) start--
|
|
|
|
var end = cursorPos
|
|
while (end < text.length && isfilec(text.charAt(end))) end++
|
|
|
|
if (start === end) return ""
|
|
return text.substring(start, end)
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
loadChat()
|
|
scrollToBottom()
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// Header - shows active session/agent and state
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 8
|
|
|
|
// Sidebar toggle button (larger touch target)
|
|
ToolButton {
|
|
text: chatPane.sidebarVisible ? "◀" : "▶"
|
|
font.pixelSize: 14
|
|
implicitWidth: 32
|
|
implicitHeight: 32
|
|
onClicked: chatPane.toggleSidebar()
|
|
ToolTip.text: chatPane.sidebarVisible ? "Hide sidebar (Ctrl+B)" : "Show sidebar (Ctrl+B)"
|
|
ToolTip.visible: hovered
|
|
}
|
|
|
|
// Active session/agent label
|
|
Label {
|
|
text: {
|
|
var sessionName = ""
|
|
var agentName = ""
|
|
for (var i = 0; i < sessionModel.count; ++i) {
|
|
var item = sessionModel.get(i)
|
|
if (item.type === "session" && item.id === ollie.activeSessionId) {
|
|
sessionName = item.displayName || item.id
|
|
}
|
|
if (item.type === "agent" && item.agentId === ollie.activeAgentId && item.parentId === ollie.activeSessionId) {
|
|
agentName = item.displayName || item.agentId
|
|
}
|
|
}
|
|
if (sessionName && agentName) {
|
|
return sessionName + " / " + agentName
|
|
} else if (sessionName) {
|
|
return sessionName + " (no agent)"
|
|
}
|
|
return "(no session)"
|
|
}
|
|
font.bold: true
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
elide: Text.ElideMiddle
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// State indicator
|
|
Rectangle {
|
|
width: 10; height: 10; radius: 5
|
|
color: {
|
|
if (!ollie.activeAgentId) return "#9e9e9e" // gray when no agent
|
|
if (ollie.activeSessionPaused) return "#9e9e9e" // gray when paused
|
|
if (ollie.activeState === "idle") return "#4caf50"
|
|
if (ollie.activeState.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: {
|
|
if (!ollie.activeAgentId) return ""
|
|
if (ollie.activeSessionPaused) return "inactive"
|
|
return ollie.activeState
|
|
}
|
|
opacity: 0.7
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Chat display — block-based ListView with scroll-to-bottom button
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
|
|
// Placeholder when no agent selected or disconnected
|
|
Label {
|
|
anchors.centerIn: parent
|
|
text: {
|
|
if (!ollie.daemonConnected) return "Not connected"
|
|
if (!ollie.activeSessionId) return "Select a session"
|
|
return "Select an agent to view chat"
|
|
}
|
|
color: theme.midColor
|
|
font.italic: true
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
visible: !ollie.activeAgentId || !ollie.daemonConnected
|
|
}
|
|
|
|
// Bypass request notification
|
|
Rectangle {
|
|
id: bypassBanner
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: 8
|
|
height: bypassLayout.implicitHeight + 16
|
|
radius: 6
|
|
color: "#fff3e0" // amber light
|
|
border.color: "#ff9800"
|
|
border.width: 1
|
|
visible: chatPane.currentBypass !== null
|
|
z: 100
|
|
|
|
ColumnLayout {
|
|
id: bypassLayout
|
|
anchors.fill: parent
|
|
anchors.margins: 8
|
|
spacing: 6
|
|
|
|
Label {
|
|
text: "⚠️ Elevation Request"
|
|
font.bold: true
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize + 2 : 14
|
|
color: "#e65100"
|
|
}
|
|
|
|
Label {
|
|
text: chatPane.currentBypass ? chatPane.currentBypass.cmd : ""
|
|
font.family: "monospace"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
wrapMode: Text.WrapAnywhere
|
|
Layout.fillWidth: true
|
|
color: "#333"
|
|
}
|
|
|
|
Label {
|
|
text: chatPane.currentBypass ? "cwd: " + chatPane.currentBypass.cwd : ""
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize - 1 : 10
|
|
color: "#666"
|
|
visible: chatPane.currentBypass && chatPane.currentBypass.cwd !== ""
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: 8
|
|
Layout.topMargin: 4
|
|
|
|
Button {
|
|
text: "Approve"
|
|
highlighted: true
|
|
onClicked: {
|
|
if (chatPane.currentBypass) {
|
|
ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
Button {
|
|
text: "Deny"
|
|
onClicked: {
|
|
if (chatPane.currentBypass) {
|
|
ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ListView {
|
|
id: chatView
|
|
model: chatModel
|
|
spacing: 4
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
visible: ollie.daemonConnected && ollie.activeAgentId !== ""
|
|
|
|
onCountChanged: {
|
|
if (!scrollButton.visible) chatPane.scrollToBottom()
|
|
}
|
|
onContentHeightChanged: {
|
|
if (!scrollButton.visible) chatPane.scrollToBottom()
|
|
}
|
|
|
|
delegate: ChatBlockDelegate {
|
|
width: chatView.width - 8
|
|
addBookmark: root.addBookmark
|
|
removeBookmark: root.removeBookmark
|
|
checkBookmarked: root.isBookmarked
|
|
}
|
|
}
|
|
|
|
// Chat overview and navigation minimap
|
|
Rectangle {
|
|
id: chatMinimap
|
|
anchors.top: chatView.top
|
|
anchors.right: parent.right
|
|
anchors.bottom: chatView.bottom
|
|
width: 24
|
|
color: Qt.rgba(theme.midColor.r, theme.midColor.g, theme.midColor.b, 0.05)
|
|
visible: chatView.visible && chatModel.count > 0
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
spacing: 1
|
|
Repeater {
|
|
model: chatModel
|
|
Rectangle {
|
|
width: chatMinimap.width
|
|
height: Math.max(2, chatMinimap.height / Math.max(1, chatModel.count) - 1)
|
|
opacity: 0.65
|
|
color: {
|
|
switch (model.blockType) {
|
|
case "user": return "#4caf50"
|
|
case "assistant": return "#2196f3"
|
|
case "error": return "#f44336"
|
|
case "tool": return "#ff9800"
|
|
default: return theme.midColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: chatView.positionViewAtIndex(
|
|
Math.floor(mouse.y / height * chatModel.count), ListView.Beginning)
|
|
}
|
|
}
|
|
Button {
|
|
id: scrollButton
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
anchors.margins: 12
|
|
text: "↓"
|
|
visible: chatView.visible && !chatView.atYEnd
|
|
onClicked: chatPane.scrollToBottom()
|
|
ToolTip.text: "Scroll to bottom"
|
|
ToolTip.visible: hovered
|
|
}
|
|
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Action toolbar — quick actions without keyboard
|
|
RowLayout {
|
|
id: actionToolbar
|
|
Layout.fillWidth: true
|
|
Layout.margins: 4
|
|
spacing: 4
|
|
visible: ollie.daemonConnected && ollie.activeAgentId !== ""
|
|
|
|
ToolButton {
|
|
text: "⏹ Stop"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitHeight: 28
|
|
enabled: ollie.activeState !== "idle"
|
|
onClicked: ollie.interrupt()
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Stop the agent (Escape)"
|
|
}
|
|
|
|
ToolButton {
|
|
text: "📦 Compact"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitHeight: 28
|
|
onClicked: ollie.submit("/compact")
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Compact context window"
|
|
}
|
|
|
|
ToolButton {
|
|
text: "📋 Copy Last"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitHeight: 28
|
|
enabled: chatModel.count > 0
|
|
onClicked: {
|
|
// Find last assistant block and copy its content
|
|
for (var i = chatModel.count - 1; i >= 0; i--) {
|
|
var block = chatModel.get(i)
|
|
if (block && block.blockType === "assistant") {
|
|
// The renderedHtml is HTML, but we want plain text
|
|
// For now, copy the HTML - TODO: strip tags
|
|
clipboard.copyText(block.blockRenderedHtml.replace(/<[^>]*>/g, ''))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Copy last assistant response"
|
|
}
|
|
|
|
// Templates dropdown
|
|
ToolButton {
|
|
id: templateButton
|
|
text: "📝 Templates ▾"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitHeight: 28
|
|
onClicked: templateMenu.popup()
|
|
|
|
Menu {
|
|
id: templateMenu
|
|
MenuItem { text: "Yes"; onTriggered: ollie.submit("Yes") }
|
|
MenuItem { text: "No"; onTriggered: ollie.submit("No") }
|
|
MenuSeparator {}
|
|
MenuItem { text: "Load skills and tools..."; onTriggered: promptInput.text = "Load all required skills and tools, then " }
|
|
MenuSeparator {}
|
|
MenuItem { text: "Continue"; onTriggered: ollie.submit("continue") }
|
|
MenuItem { text: "Retry"; onTriggered: ollie.submit("retry") }
|
|
}
|
|
}
|
|
|
|
// Processes button
|
|
ToolButton {
|
|
text: "⚙️ Procs"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitHeight: 28
|
|
onClicked: procManagerWindow.show()
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Background processes"
|
|
}
|
|
|
|
// Peers button
|
|
ToolButton {
|
|
text: "🔗 Peers"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitHeight: 28
|
|
onClicked: peerManagerWindow.show()
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Manage peer connections"
|
|
}
|
|
|
|
Item { Layout.fillWidth: true } // Spacer
|
|
}
|
|
|
|
// Input area
|
|
RowLayout {
|
|
id: inputRow
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 4
|
|
|
|
function doSubmit() {
|
|
if (promptInput.text.trim() !== "") {
|
|
ollie.submit(promptInput.text.trim())
|
|
promptInput.text = ""
|
|
}
|
|
}
|
|
|
|
// Clear button
|
|
ToolButton {
|
|
text: "✕"
|
|
font.pixelSize: 12
|
|
implicitWidth: 28
|
|
implicitHeight: 28
|
|
Layout.alignment: Qt.AlignBottom
|
|
visible: promptInput.text.length > 0
|
|
onClicked: promptInput.text = ""
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Clear input"
|
|
}
|
|
|
|
// TextArea in ScrollView for overflow, with mouse chording
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.maximumHeight: 200
|
|
Layout.preferredHeight: Math.min(promptInput.contentHeight + 16, 200)
|
|
clip: true
|
|
|
|
ScrollView {
|
|
id: promptScroll
|
|
anchors.fill: parent
|
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
|
|
|
TextArea {
|
|
id: promptInput
|
|
width: promptScroll.width
|
|
placeholderText: {
|
|
if (!ollie.daemonConnected) return "Not connected"
|
|
if (!ollie.activeSessionId) return "No session"
|
|
if (!ollie.activeAgentId) return "No agent"
|
|
if (ollie.activeSessionPaused) return "Session paused"
|
|
return "B1=sel B2=exec B3=plumb B1+B2=cut B1+B3=paste"
|
|
}
|
|
enabled: ollie.daemonConnected && ollie.activeSessionId !== "" && ollie.activeAgentId !== "" && !ollie.activeSessionPaused
|
|
wrapMode: TextEdit.Wrap
|
|
selectByMouse: true
|
|
persistentSelection: true
|
|
|
|
property string _lastText: ""
|
|
property int _lastCursorPos: 0
|
|
|
|
onTextChanged: {
|
|
_lastText = text
|
|
_lastCursorPos = cursorPosition
|
|
}
|
|
|
|
Keys.onReturnPressed: function(event) {
|
|
if (event.modifiers & Qt.ShiftModifier) {
|
|
event.accepted = false
|
|
} else {
|
|
inputRow.doSubmit()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
// Double-click: bracket/quote pair or word select
|
|
TapHandler {
|
|
acceptedButtons: Qt.LeftButton
|
|
onDoubleTapped: function(eventPoint, button) {
|
|
var pos = eventPoint.position
|
|
if (!TextSelection.selectBracketPair(promptInput, pos.x, pos.y)) {
|
|
TextSelection.selectWordAt(promptInput, pos.x, pos.y)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Acme-style mouse chording overlay
|
|
// B2 = execute selection
|
|
// B3 = plumb selection / context menu
|
|
// B1+B2 = cut (snarf)
|
|
// B1+B3 = paste
|
|
MouseArea {
|
|
id: chordArea
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.MiddleButton | Qt.RightButton
|
|
propagateComposedEvents: true
|
|
cursorShape: Qt.IBeamCursor
|
|
|
|
// Track if B1 (left) is currently held for chord detection
|
|
property bool leftHeld: false
|
|
|
|
onPressed: function(mouse) {
|
|
// Check if this is a chord (B1 already held)
|
|
if (mouse.buttons & Qt.LeftButton) {
|
|
leftHeld = true
|
|
if (mouse.button === Qt.MiddleButton) {
|
|
promptInput.cut()
|
|
mouse.accepted = true
|
|
return
|
|
} else if (mouse.button === Qt.RightButton) {
|
|
promptInput.paste()
|
|
mouse.accepted = true
|
|
return
|
|
}
|
|
}
|
|
leftHeld = false
|
|
mouse.accepted = false
|
|
}
|
|
|
|
onClicked: function(mouse) {
|
|
if (leftHeld) {
|
|
leftHeld = false
|
|
return
|
|
}
|
|
|
|
if (mouse.button === Qt.MiddleButton) {
|
|
var sel = promptInput.selectedText
|
|
if (sel && sel.trim() !== "") {
|
|
ollie.submit(sel.trim())
|
|
}
|
|
} else if (mouse.button === Qt.RightButton) {
|
|
if (!plumber.available()) return
|
|
var sel = promptInput.selectedText
|
|
if (sel && sel.trim() !== "") {
|
|
plumber.plumb(sel.trim(), sessionCwd())
|
|
} else {
|
|
var mappedPos = chordArea.mapToItem(promptInput, mouse.x, mouse.y)
|
|
var pos = promptInput.positionAt(mappedPos.x, mappedPos.y)
|
|
var word = wordAtCursor(promptInput, pos)
|
|
if (word) plumber.plumb(word, sessionCwd())
|
|
}
|
|
}
|
|
}
|
|
|
|
onReleased: {
|
|
leftHeld = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Paste button (mouse-first workflow)
|
|
ToolButton {
|
|
text: "📋"
|
|
font.pixelSize: 14
|
|
implicitWidth: 36
|
|
implicitHeight: 36
|
|
Layout.alignment: Qt.AlignBottom
|
|
enabled: promptInput.enabled
|
|
onClicked: promptInput.paste()
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Paste from clipboard"
|
|
}
|
|
|
|
// Send button (larger touch target)
|
|
Button {
|
|
text: "▶"
|
|
font.pixelSize: 16
|
|
Layout.alignment: Qt.AlignBottom
|
|
Layout.preferredWidth: 48
|
|
Layout.preferredHeight: 48
|
|
focusPolicy: Qt.NoFocus
|
|
enabled: promptInput.enabled && promptInput.text.trim() !== ""
|
|
onClicked: inputRow.doSubmit()
|
|
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: "Send (Enter)"
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Connections {
|
|
target: chatModel
|
|
function onCountChanged() { chatPane.trimVisibleBlocks() }
|
|
}
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onActiveSessionIdChanged() {
|
|
if (!ollie.activeAgentId) chatPane.loadChat()
|
|
// Update current bypass for the new active agent
|
|
chatPane.updateCurrentBypass()
|
|
}
|
|
function onActiveAgentIdChanged() {
|
|
if (ollie.activeAgentId) chatPane.loadChat()
|
|
// Update current bypass for the new active agent
|
|
chatPane.updateCurrentBypass()
|
|
}
|
|
function onBypassRequested(sessionId, agentId, id, cmd, cwd) {
|
|
// Add to pending bypasses map
|
|
var newMap = chatPane.pendingBypasses
|
|
newMap[id] = {
|
|
sessionId: sessionId,
|
|
agentId: agentId,
|
|
id: id,
|
|
cmd: cmd,
|
|
cwd: cwd
|
|
}
|
|
chatPane.pendingBypasses = newMap
|
|
chatPane.updateCurrentBypass()
|
|
}
|
|
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.currentBypass) {
|
|
ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, true)
|
|
}
|
|
}
|
|
function denyBypass() {
|
|
if (chatPane.currentBypass) {
|
|
ollie.resolveBypass(chatPane.currentBypass.sessionId, chatPane.currentBypass.agentId, chatPane.currentBypass.id, false)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|