ollie/kde/gui/main.qml

358 lines
12 KiB
QML

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