507 lines
24 KiB
QML
507 lines
24 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
|
|
Item {
|
|
id: tree
|
|
property bool visiblePane: true
|
|
property int renamingIndex: -1
|
|
property string renamingText: ""
|
|
property string killTargetSessionId: ""
|
|
property string killTargetSessionName: ""
|
|
property string killTargetAgentSessionId: ""
|
|
property string killTargetAgentId: ""
|
|
signal newSessionRequested()
|
|
signal settingsRequested()
|
|
signal sessionSettingsRequested(string sessionName)
|
|
signal newAgentRequested(string sessionId)
|
|
signal agentSettingsRequested(string sessionId, string agentId)
|
|
signal goalRequested(string sessionId, string sessionName)
|
|
signal renameFailed(string itemType, string attemptedName)
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
id: sidebarPane
|
|
visible: tree.visiblePane
|
|
spacing: 0
|
|
|
|
ToolBar {
|
|
Layout.fillWidth: true
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
Label { text: "Sessions"; Layout.fillWidth: true; font.bold: true; leftPadding: 8 }
|
|
ToolButton {
|
|
text: "\u2795"
|
|
onClicked: tree.newSessionRequested()
|
|
ToolTip.text: "New Session"
|
|
ToolTip.visible: hovered
|
|
}
|
|
ToolButton {
|
|
text: "\u21BB"
|
|
onClicked: ollie.refreshSessions()
|
|
}
|
|
ToolButton {
|
|
text: "\u2699"
|
|
onClicked: tree.settingsRequested()
|
|
ToolTip.text: "Settings"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
}
|
|
|
|
ListView {
|
|
id: sessionList
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.minimumHeight: 100
|
|
model: sessionModel
|
|
clip: true
|
|
focus: false
|
|
activeFocusOnTab: false
|
|
|
|
// Show placeholder when empty or disconnected
|
|
Label {
|
|
anchors.centerIn: parent
|
|
text: ollie.daemonConnected ? "(no sessions)" : "(not connected)"
|
|
color: theme.midColor
|
|
font.italic: true
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 12
|
|
visible: sessionList.count === 0 || !ollie.daemonConnected
|
|
activeFocusOnTab: false
|
|
}
|
|
|
|
delegate: ItemDelegate {
|
|
width: sessionList.width
|
|
focusPolicy: Qt.NoFocus
|
|
activeFocusOnTab: false
|
|
|
|
// Guard against undefined during initial render
|
|
height: {
|
|
if (!type) return 0
|
|
if (type === "header") return 24
|
|
if (type === "agent" && !parentExpanded)
|
|
return 0
|
|
return 28
|
|
}
|
|
visible: height > 0
|
|
property int modelIndex: index
|
|
property bool isHeader: type === "header"
|
|
|
|
// Headers are not interactive
|
|
enabled: !isHeader
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
hoverEnabled: false
|
|
enabled: !parent.isHeader
|
|
onClicked: function(mouse) {
|
|
if (mouse.button === Qt.RightButton) {
|
|
if (type === "session") {
|
|
var item = sessionModel.get(parent.modelIndex)
|
|
if (item) {
|
|
tree.killTargetSessionId = item.id
|
|
tree.killTargetSessionName = item.baseName
|
|
}
|
|
sessionCtxMenu.popup()
|
|
} else if (type === "agent") {
|
|
var agentItem = sessionModel.get(parent.modelIndex)
|
|
if (agentItem) {
|
|
tree.killTargetAgentSessionId = agentItem.parentId
|
|
tree.killTargetAgentId = agentItem.agentId
|
|
}
|
|
agentCtxMenu.popup()
|
|
}
|
|
return
|
|
}
|
|
if (renamingIndex >= 0) return
|
|
if (type === "session") {
|
|
// Find first agent in this session and select it
|
|
var foundAgent = null
|
|
for (var i = 0; i < sessionModel.count; ++i) {
|
|
var item = sessionModel.get(i)
|
|
if (item.type === "agent" && item.parentId === id) {
|
|
foundAgent = item
|
|
break
|
|
}
|
|
}
|
|
if (foundAgent) {
|
|
// Select the first agent
|
|
ollie.switchAgent(id, foundAgent.agentId)
|
|
} else {
|
|
// No agents - just select session (clears agent)
|
|
ollie.setActiveSessionId(id)
|
|
}
|
|
sessionModel.setExpanded(parent.modelIndex, !expanded)
|
|
} else if (type === "agent") {
|
|
ollie.switchAgent(parentId, agentId)
|
|
// Toggle expansion if agent has children
|
|
if (hasChildren) {
|
|
sessionModel.setExpanded(parent.modelIndex, !expanded)
|
|
}
|
|
}
|
|
}
|
|
onDoubleClicked: {
|
|
if (type === "session" || type === "agent") {
|
|
startRename(parent.modelIndex)
|
|
}
|
|
}
|
|
|
|
Menu {
|
|
id: sessionCtxMenu
|
|
MenuItem {
|
|
text: "Goal..."
|
|
onClicked: {
|
|
tree.goalRequested(tree.killTargetSessionId, tree.killTargetSessionName)
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: "Settings..."
|
|
onClicked: {
|
|
tree.sessionSettingsRequested(tree.killTargetSessionName)
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: "Open Workdir"
|
|
onClicked: {
|
|
if (tree.killTargetSessionName) {
|
|
var cfg = ollie.readSessionConfig(tree.killTargetSessionName)
|
|
if (cfg.cwd) ollie.openDirectory(cfg.cwd)
|
|
}
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: "New Agent..."
|
|
onClicked: {
|
|
tree.newAgentRequested(tree.killTargetSessionId)
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: ollie.isSessionPaused(tree.killTargetSessionId) ? "Resume" : "Pause"
|
|
onClicked: {
|
|
if (tree.killTargetSessionId) {
|
|
if (ollie.isSessionPaused(tree.killTargetSessionId))
|
|
ollie.resumeSession(tree.killTargetSessionId)
|
|
else
|
|
ollie.pauseSession(tree.killTargetSessionId)
|
|
}
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: "Kill Session"
|
|
onClicked: {
|
|
if (tree.killTargetSessionId)
|
|
ollie.killSession(tree.killTargetSessionId)
|
|
ollie.refreshSessions()
|
|
}
|
|
}
|
|
}
|
|
|
|
Menu {
|
|
id: agentCtxMenu
|
|
MenuItem {
|
|
text: "Settings..."
|
|
onClicked: {
|
|
if (tree.killTargetAgentSessionId && tree.killTargetAgentId)
|
|
tree.agentSettingsRequested(tree.killTargetAgentSessionId, tree.killTargetAgentId)
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: "Open Workdir"
|
|
onClicked: {
|
|
if (tree.killTargetAgentSessionId && tree.killTargetAgentId) {
|
|
var cfg = ollie.readAgentConfig(tree.killTargetAgentSessionId, tree.killTargetAgentId)
|
|
if (cfg.cwd) ollie.openDirectory(cfg.cwd)
|
|
}
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: "Kill"
|
|
onClicked: {
|
|
if (tree.killTargetAgentSessionId && tree.killTargetAgentId)
|
|
ollie.killAgent(tree.killTargetAgentSessionId, tree.killTargetAgentId)
|
|
ollie.refreshSessions()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
contentItem: RowLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// Indent + expand/collapse indicator
|
|
Item {
|
|
Layout.preferredWidth: type === "header" ? 8 : (level || 0) * 16 + 16
|
|
Layout.preferredHeight: type === "header" ? 24 : 28
|
|
|
|
Label {
|
|
anchors.centerIn: parent
|
|
text: (type === "session" || hasChildren) ? (expanded ? "▼" : "▶") : ""
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
|
|
color: theme.textColor
|
|
opacity: 0.6
|
|
visible: type !== "header"
|
|
}
|
|
}
|
|
|
|
// Connection status indicator (session rows only)
|
|
Rectangle {
|
|
Layout.preferredWidth: 8
|
|
Layout.preferredHeight: 8
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.rightMargin: 6
|
|
radius: 4
|
|
color: connectionColor || "gray"
|
|
visible: type === "session"
|
|
}
|
|
|
|
// Pending bypass indicator (agent rows only)
|
|
Label {
|
|
id: bypassIndicator
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.rightMargin: 4
|
|
text: "⚠"
|
|
color: "#e6a700"
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
visible: type === "agent" && ollie.pendingBypassCount(parentId || "", agentId || "") > 0
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onPendingBypassCountChanged(sid, aid) {
|
|
if (type === "agent" && sid === parentId && aid === agentId) {
|
|
bypassIndicator.visible = ollie.pendingBypassCount(parentId || "", agentId || "") > 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Name area
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: type === "header" ? 24 : 28
|
|
|
|
TextField {
|
|
anchors.fill: parent
|
|
text: (modelIndex === renamingIndex) ? renamingText : ""
|
|
visible: modelIndex === renamingIndex
|
|
verticalAlignment: Text.AlignVCenter
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
color: theme.textColor
|
|
background: Rectangle {
|
|
color: theme.baseColor
|
|
border.color: theme.highlightColor
|
|
radius: 2
|
|
}
|
|
onTextChanged: {
|
|
if (modelIndex === renamingIndex) {
|
|
renamingText = text
|
|
}
|
|
}
|
|
Keys.onReturnPressed: commitRename()
|
|
Keys.onEscapePressed: cancelRename()
|
|
onAccepted: commitRename()
|
|
onVisibleChanged: {
|
|
if (visible)
|
|
selectAll()
|
|
}
|
|
}
|
|
|
|
Label {
|
|
anchors.fill: parent
|
|
text: displayName || ""
|
|
color: type === "header" ? theme.midColor : theme.textColor
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * (type === "header" ? 0.85 : 1) : Qt.application.font.pointSize
|
|
font.bold: type === "header"
|
|
font.capitalization: type === "header" ? Font.AllUppercase : Font.MixedCase
|
|
verticalAlignment: Text.AlignVCenter
|
|
visible: modelIndex !== renamingIndex
|
|
}
|
|
}
|
|
|
|
// Agent state indicator (agent rows only)
|
|
Rectangle {
|
|
id: stateIndicator
|
|
Layout.preferredWidth: height
|
|
Layout.preferredHeight: parent.height * 0.3
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.leftMargin: parent.height * 0.15
|
|
Layout.rightMargin: parent.height * 0.25
|
|
radius: height / 2
|
|
visible: type === "agent"
|
|
color: {
|
|
// Session paused overrides agent state
|
|
if (ollie.isSessionPaused(parentId || "")) return "#808080" // gray - session paused
|
|
var state = ollie.agentState(parentId || "", agentId || "")
|
|
if (state.startsWith("calling")) return "#f0a030" // orange - calling tool
|
|
if (state === "thinking") return "#3080f0" // blue - thinking
|
|
if (state === "paused") return "#808080" // gray - paused
|
|
return "#40c040" // green - idle
|
|
}
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onAgentStateChanged(sid, aid, state) {
|
|
if (type === "agent" && sid === parentId && aid === agentId) {
|
|
stateIndicator.color = stateIndicator.computeColor()
|
|
}
|
|
}
|
|
function onSessionsChanged() {
|
|
if (type === "agent") {
|
|
stateIndicator.color = stateIndicator.computeColor()
|
|
}
|
|
}
|
|
}
|
|
|
|
function computeColor() {
|
|
if (ollie.isSessionPaused(parentId || "")) return "#808080"
|
|
var state = ollie.agentState(parentId || "", agentId || "")
|
|
if (state.startsWith("calling")) return "#f0a030"
|
|
else if (state === "thinking") return "#3080f0"
|
|
else if (state === "paused") return "#808080"
|
|
else return "#40c040"
|
|
}
|
|
}
|
|
}
|
|
|
|
highlighted: (type === "agent" && agentId === ollie.activeAgentId && parentId === ollie.activeSessionId)
|
|
}
|
|
}
|
|
|
|
// --- Bookmarks Section ---
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: theme.midColor
|
|
visible: bookmarksSection.visible
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: bookmarksSection
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: bookmarksExpanded ? Math.min(150, bookmarksList.contentHeight + 30) : 30
|
|
visible: bookmarksModel.count > 0
|
|
spacing: 0
|
|
|
|
property bool bookmarksExpanded: true
|
|
|
|
ToolBar {
|
|
Layout.fillWidth: true
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
ToolButton {
|
|
text: bookmarksSection.bookmarksExpanded ? "▼" : "▶"
|
|
implicitWidth: 24
|
|
onClicked: bookmarksSection.bookmarksExpanded = !bookmarksSection.bookmarksExpanded
|
|
}
|
|
Label {
|
|
text: "Bookmarks (" + bookmarksModel.count + ")"
|
|
Layout.fillWidth: true
|
|
font.bold: true
|
|
}
|
|
ToolButton {
|
|
text: "✕"
|
|
implicitWidth: 24
|
|
onClicked: bookmarksModel.clear()
|
|
ToolTip.text: "Clear all bookmarks"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
}
|
|
|
|
ListView {
|
|
id: bookmarksList
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
model: bookmarksModel
|
|
clip: true
|
|
visible: bookmarksSection.bookmarksExpanded
|
|
|
|
delegate: ItemDelegate {
|
|
width: bookmarksList.width
|
|
height: theme.fontSize > 0 ? theme.fontSize * 3 : 36
|
|
|
|
property string ollieUrl: "ollie://" + encodeURIComponent(sessionName) + "/" + encodeURIComponent(agentName) + "#" + blockIndex
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: theme.fontSize > 0 ? theme.fontSize * 0.6 : 8
|
|
anchors.rightMargin: theme.fontSize > 0 ? theme.fontSize * 0.3 : 4
|
|
spacing: theme.fontSize > 0 ? theme.fontSize * 0.3 : 4
|
|
|
|
Column {
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
|
|
Label {
|
|
text: sessionName + "/" + agentName
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.75 : 9
|
|
color: theme.midColor
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
Label {
|
|
text: "[" + blockType + "] " + preview
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
ToolButton {
|
|
text: "📋"
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitWidth: theme.fontSize > 0 ? theme.fontSize * 2 : 28
|
|
implicitHeight: theme.fontSize > 0 ? theme.fontSize * 1.8 : 24
|
|
onClicked: clipboard.setText(ollieUrl)
|
|
ToolTip.visible: hovered
|
|
ToolTip.delay: 500
|
|
ToolTip.text: "Copy ollie:// link"
|
|
}
|
|
|
|
ToolButton {
|
|
text: "✕"
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.9 : 11
|
|
implicitWidth: theme.fontSize > 0 ? theme.fontSize * 2 : 28
|
|
implicitHeight: theme.fontSize > 0 ? theme.fontSize * 1.8 : 24
|
|
onClicked: root.removeBookmark(sessionId, agentId, blockIndex)
|
|
ToolTip.visible: hovered
|
|
ToolTip.delay: 500
|
|
ToolTip.text: "Remove bookmark"
|
|
}
|
|
}
|
|
|
|
onClicked: {
|
|
// Switch to the session/agent and scroll to block
|
|
ollie.switchAgent(sessionId, agentId)
|
|
// Signal to ChatPane to scroll to blockIndex
|
|
chatPane.scrollToBlock(blockIndex)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function startRename(index) {
|
|
var item = sessionModel.get(index)
|
|
if (item.type === "session" || item.type === "agent") {
|
|
tree.renamingIndex = index
|
|
tree.renamingText = item.type === "session" ? (item.baseName || item.displayName) : item.displayName
|
|
}
|
|
}
|
|
|
|
function commitRename() {
|
|
if (tree.renamingIndex < 0) return
|
|
var item = sessionModel.get(tree.renamingIndex)
|
|
var name = tree.renamingText.trim()
|
|
if (name !== "") {
|
|
var ok = item.type === "session" ? ollie.renameSession(item.id, name) : ollie.renameAgent(item.parentId, item.agentId, name)
|
|
if (!ok) tree.renameFailed(item.type, name)
|
|
}
|
|
tree.renamingIndex = -1
|
|
tree.renamingText = ""
|
|
}
|
|
|
|
function cancelRename() { tree.renamingIndex = -1; tree.renamingText = "" }
|
|
|
|
}
|