import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 ApplicationWindow { id: root visible: true width: 1000 height: 600 minimumWidth: 320 minimumHeight: 240 title: "Ollie" font.family: theme.fontFamily || Qt.application.font.family font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize // Bookmarks model (non-persistent, GUI state only) // Each entry: { sessionId, sessionName, agentId, agentName, blockId, blockType, preview } ListModel { id: bookmarksModel } function addBookmark(sessionId, sessionName, agentId, agentName, blockId, blockType, preview) { // Check for duplicate for (var i = 0; i < bookmarksModel.count; i++) { var b = bookmarksModel.get(i) if (b.sessionId === sessionId && b.agentId === agentId && b.blockId === blockId) { return false } } bookmarksModel.append({ sessionId: sessionId, sessionName: sessionName, agentId: agentId, agentName: agentName, blockId: blockId, blockType: blockType, preview: preview.substring(0, 80) + (preview.length > 80 ? "…" : "") }) return true } function removeBookmark(sessionId, agentId, blockId) { for (var i = 0; i < bookmarksModel.count; i++) { var b = bookmarksModel.get(i) if (b.sessionId === sessionId && b.agentId === agentId && b.blockId === blockId) { bookmarksModel.remove(i) return true } } return false } function isBookmarked(sessionId, agentId, blockId) { for (var i = 0; i < bookmarksModel.count; i++) { var b = bookmarksModel.get(i) if (b.sessionId === sessionId && b.agentId === agentId && b.blockId === blockId) { return true } } return false } // Handle plumb messages from the ollie port // URL format: ollie://session/agent#blockId function handleOllieMessage(url) { // Parse ollie://session/agent#blockId var match = url.match(/^ollie:\/\/([^\/]+)(?:\/([^#]+))?(?:#([a-f0-9]+))?$/) if (!match) { console.log("Invalid ollie URL:", url) return } var sessionName = decodeURIComponent(match[1]) var agentName = match[2] ? decodeURIComponent(match[2]) : "" var blockId = match[3] || "" // If blockId is present, fetch and append the block content to current chat // Don't navigate — user wants to see the block in their current context if (blockId) { var block = ollie.fetchBlock(sessionName, agentName, blockId) if (block) { chatModel.appendData(block) } return } // No blockId — navigate to session/agent var sessions = ollie.sessions for (var i = 0; i < sessions.length; i++) { var s = sessions[i] if (s.name === sessionName) { ollie.setActiveSessionId(s.id) // If agent specified, find and switch to it if (agentName && s.agents) { for (var j = 0; j < s.agents.length; j++) { var a = s.agents[j] if (a.name === agentName) { ollie.setActiveAgentId(a.id) break } } } // Raise window root.show() root.raise() root.requestActivate() return } } console.log("Session not found:", sessionName) } Connections { target: plumber function onOllieMessage(url) { handleOllieMessage(url) } } // Apply theme palette to all Qt Quick Controls palette.active.window: theme.windowColor palette.active.windowText: theme.windowTextColor palette.active.base: theme.baseColor palette.active.text: theme.textColor palette.active.button: theme.buttonColor palette.active.buttonText: theme.buttonTextColor palette.active.highlight: theme.highlightColor palette.active.highlightedText: theme.highlightedTextColor palette.active.mid: theme.midColor palette.active.dark: theme.darkColor palette.active.light: theme.lightColor palette.active.shadow: theme.shadowColor palette.active.link: theme.linkColor palette.active.toolTipBase: theme.tooltipBaseColor palette.active.toolTipText: theme.tooltipTextColor palette.active.placeholderText: theme.placeholderTextColor palette.active.alternateBase: theme.alternateBaseColor palette.inactive.window: theme.windowColor palette.inactive.windowText: theme.windowTextColor palette.inactive.base: theme.baseColor palette.inactive.text: theme.textColor palette.inactive.button: theme.buttonColor palette.inactive.buttonText: theme.buttonTextColor palette.inactive.highlight: theme.highlightColor palette.inactive.highlightedText: theme.highlightedTextColor palette.inactive.mid: theme.midColor palette.inactive.dark: theme.darkColor palette.inactive.light: theme.lightColor palette.inactive.shadow: theme.shadowColor palette.inactive.link: theme.linkColor palette.inactive.toolTipBase: theme.tooltipBaseColor palette.inactive.toolTipText: theme.tooltipTextColor palette.inactive.placeholderText: theme.placeholderTextColor palette.inactive.alternateBase: theme.alternateBaseColor palette.disabled.window: theme.windowColor palette.disabled.windowText: theme.midColor palette.disabled.base: theme.baseColor palette.disabled.text: theme.midColor palette.disabled.button: theme.buttonColor palette.disabled.buttonText: theme.midColor palette.disabled.highlight: theme.highlightColor palette.disabled.highlightedText: theme.highlightedTextColor palette.disabled.mid: theme.midColor palette.disabled.dark: theme.darkColor palette.disabled.light: theme.lightColor palette.disabled.shadow: theme.shadowColor Shortcut { sequence: "Ctrl+B" onActivated: root.sidebarVisible = !root.sidebarVisible } Shortcut { sequence: "Escape" onActivated: { if (sessionTree.renamingIndex >= 0) { sessionTree.cancelRename() } else { // Always send stop - server-side Interrupt() is safe to call // on an idle agent (no-op). This avoids races where the GUI's // cached activeState is stale. ollie.interrupt() } } } // Bypass approval/denial shortcuts (configurable in Settings) Shortcut { sequence: theme.approveBypassKey enabled: theme.approveBypassKey !== "" && chatPane.hasPendingBypass onActivated: chatPane.approveBypass() } Shortcut { sequence: theme.denyBypassKey enabled: theme.denyBypassKey !== "" && chatPane.hasPendingBypass onActivated: chatPane.denyBypass() } property bool sidebarVisible: true property bool quitting: false onClosing: function(close) { if (!quitting && theme.closeToTray) { close.accepted = false root.hide() trayIcon.show() } } Dialog { id: renameErrorDialog title: "Rename Failed" width: 320 modal: false x: (parent ? (parent.width - width) / 2 : 0) y: (parent ? (parent.height - height) / 2 : 0) // Keep the non-modal dialog visually separate from the application behind it. background: Item { // A dark offset layer reads as a shadow instead of a bright outline. Rectangle { x: 4 y: 6 width: parent.width height: parent.height color: Qt.rgba(0, 0, 0, 0.28) radius: 4 } Rectangle { anchors.fill: parent color: theme.windowColor border.color: theme.dialogOutlineColor border.width: 3 radius: 4 Rectangle { anchors.fill: parent anchors.margins: 3 color: "transparent" border.color: theme.lightColor border.width: 1 radius: 3 } } } header: Rectangle { implicitHeight: 36 color: theme.buttonColor Text { anchors.centerIn: parent; text: renameErrorDialog.title; color: theme.buttonTextColor } MouseArea { anchors.fill: parent cursorShape: Qt.SizeAllCursor property point pressPosition onPressed: pressPosition = Qt.point(mouse.x, mouse.y) onPositionChanged: if (pressed) { renameErrorDialog.x += mouse.x - pressPosition.x renameErrorDialog.y += mouse.y - pressPosition.y } } } standardButtons: Dialog.Ok // Dialogs never request keyboard focus. focus: false property string itemType: "session" property string attemptedName: "" contentItem: Label { text: "Could not rename " + renameErrorDialog.itemType + " to \"" + renameErrorDialog.attemptedName + "\".\n\nThe name may already be in use." wrapMode: Text.Wrap } } // --- Settings Dialog --- SettingsDialog { id: settingsDialog } // --- New Session Dialog --- NewSessionDialog { id: newSessionDialog } // --- New Agent Dialog --- NewAgentDialog { id: newAgentDialog targetSessionId: "" } // --- Goal Dialog --- GoalDialog { id: goalDialog } // --- Process Manager Window --- ProcManagerWindow { id: procManagerWindow visible: false } // --- Peer Manager Window --- PeerManagerWindow { id: peerManagerWindow visible: false } SplitView { anchors.fill: parent SessionTree { id: sessionTree SplitView.preferredWidth: root.sidebarVisible ? root.width * 0.15 : 0 SplitView.minimumWidth: 0 visiblePane: root.sidebarVisible onNewSessionRequested: { newSessionDialog.editMode = false newSessionDialog.editSessionName = "" newSessionDialog.open() } onSettingsRequested: settingsDialog.open() onSessionSettingsRequested: function(sessionName) { newSessionDialog.openForSession(sessionName) } onNewAgentRequested: function(sessionId) { newAgentDialog.editMode = false newAgentDialog.editAgentId = "" newAgentDialog.targetSessionId = sessionId newAgentDialog.open() } onAgentSettingsRequested: function(sessionId, agentId) { newAgentDialog.openForAgent(sessionId, agentId) } onGoalRequested: function(sessionId, sessionName) { goalDialog.sessionId = sessionId goalDialog.sessionName = sessionName goalDialog.open() } onRenameFailed: function(itemType, attemptedName) { renameErrorDialog.itemType = itemType renameErrorDialog.attemptedName = attemptedName renameErrorDialog.open() } } ChatPane { id: chatPane SplitView.fillWidth: true SplitView.fillHeight: true sidebarVisible: root.sidebarVisible onToggleSidebar: root.sidebarVisible = !root.sidebarVisible } } }