358 lines
12 KiB
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
|
|
}
|
|
}
|
|
}
|