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 killTargetAgentSessionId: "" property string killTargetAgentId: "" signal newSessionRequested() signal settingsRequested() signal newAgentRequested(string sessionId) 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 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 // 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 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) } } onDoubleClicked: { if (type === "session" || type === "agent") { startRename(parent.modelIndex) } } Menu { id: sessionCtxMenu 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: "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" ? (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" } // 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) { forceActiveFocus() 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 } } } highlighted: (type === "agent" && agentId === ollie.activeAgentId && parentId === ollie.activeSessionId) } } } 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 = "" } }