diff --git a/kde/gui/ChatBlockDelegate.qml b/kde/gui/ChatBlockDelegate.qml index 5817c61..de44fd9 100644 --- a/kde/gui/ChatBlockDelegate.qml +++ b/kde/gui/ChatBlockDelegate.qml @@ -17,6 +17,16 @@ Column { required property string blockName required property var blockSegments required property string blockRenderedHtml + required property int index + + // Block index for bookmarking + property int blockIndex: index + + // Bookmark functions (connected from parent) + property var addBookmark: function(sid, sn, aid, an, bi, bt, bn, p) { return false } + property var removeBookmark: function(sid, aid, bi) { return false } + property var checkBookmarked: function(sid, aid, bi) { return false } + property bool isBookmarked: checkBookmarked(ollie.activeSessionId, ollie.activeAgentId, blockIndex) // Expand state based on block type property bool blockExpanded: { @@ -179,6 +189,35 @@ Column { if (content) clipboard.copyText(content) } } + + // Bookmark + ToolButton { + text: root.isBookmarked ? "🔖" : "📌" + font.pixelSize: 12 + implicitWidth: 24 + implicitHeight: 22 + ToolTip.visible: hovered + ToolTip.text: root.isBookmarked ? "Remove bookmark" : "Bookmark" + onClicked: { + if (root.isBookmarked) { + root.removeBookmark(ollie.activeSessionId, ollie.activeAgentId, root.blockIndex) + } else { + var sessionName = ollie.activeSessionName || ollie.activeSessionId + var agentName = ollie.activeAgentName || ollie.activeAgentId + var preview = getBlockPlainText() || blockName + root.addBookmark( + ollie.activeSessionId, + sessionName, + ollie.activeAgentId, + agentName, + root.blockIndex, + blockType, + blockName, + preview + ) + } + } + } } } } diff --git a/kde/gui/ChatPane.qml b/kde/gui/ChatPane.qml index 79dfded..05f623f 100644 --- a/kde/gui/ChatPane.qml +++ b/kde/gui/ChatPane.qml @@ -37,6 +37,11 @@ Item { function scrollToBottom() { chatView.positionViewAtEnd() } + function scrollToBlock(blockIndex) { + if (blockIndex >= 0 && blockIndex < chatModel.count) { + chatView.positionViewAtIndex(blockIndex, ListView.Center) + } + } function trimVisibleBlocks() { if (chatModel.count > chatPane.maxVisibleBlocks) chatModel.trimFront(chatPane.maxVisibleBlocks) @@ -265,6 +270,9 @@ Item { delegate: ChatBlockDelegate { width: chatView.width - 8 + addBookmark: root.addBookmark + removeBookmark: root.removeBookmark + checkBookmarked: root.isBookmarked } } diff --git a/kde/gui/SessionTree.qml b/kde/gui/SessionTree.qml index e93e503..a30ed38 100644 --- a/kde/gui/SessionTree.qml +++ b/kde/gui/SessionTree.qml @@ -53,6 +53,7 @@ Item { id: sessionList Layout.fillWidth: true Layout.fillHeight: true + Layout.minimumHeight: 100 model: sessionModel clip: true focus: false @@ -366,6 +367,102 @@ Item { 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: 36 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 4 + spacing: 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: "✕" + implicitWidth: 20 + implicitHeight: 20 + onClicked: root.removeBookmark(sessionId, agentId, blockIndex) + } + } + + 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) diff --git a/kde/gui/main.qml b/kde/gui/main.qml index 5286bb7..26569e6 100644 --- a/kde/gui/main.qml +++ b/kde/gui/main.qml @@ -13,6 +13,54 @@ ApplicationWindow { 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, blockIndex, blockType, blockName, preview } + ListModel { + id: bookmarksModel + } + + function addBookmark(sessionId, sessionName, agentId, agentName, blockIndex, blockType, blockName, 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.blockIndex === blockIndex) { + return false + } + } + bookmarksModel.append({ + sessionId: sessionId, + sessionName: sessionName, + agentId: agentId, + agentName: agentName, + blockIndex: blockIndex, + blockType: blockType, + blockName: blockName, + preview: preview.substring(0, 80) + (preview.length > 80 ? "…" : "") + }) + return true + } + + function removeBookmark(sessionId, agentId, blockIndex) { + for (var i = 0; i < bookmarksModel.count; i++) { + var b = bookmarksModel.get(i) + if (b.sessionId === sessionId && b.agentId === agentId && b.blockIndex === blockIndex) { + bookmarksModel.remove(i) + return true + } + } + return false + } + + function isBookmarked(sessionId, agentId, blockIndex) { + for (var i = 0; i < bookmarksModel.count; i++) { + var b = bookmarksModel.get(i) + if (b.sessionId === sessionId && b.agentId === agentId && b.blockIndex === blockIndex) { + return true + } + } + return false + } + // Apply theme palette to all Qt Quick Controls palette.active.window: theme.windowColor palette.active.windowText: theme.windowTextColor diff --git a/kde/gui/ollie9pclient.cpp b/kde/gui/ollie9pclient.cpp index 7fd589a..0a5ad60 100644 --- a/kde/gui/ollie9pclient.cpp +++ b/kde/gui/ollie9pclient.cpp @@ -971,6 +971,36 @@ void Ollie9pClient::openDirectory(const QString &path) QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } +QString Ollie9pClient::activeSessionName() const +{ + if (m_activeSessionId.isEmpty()) return QString(); + for (const QVariant &v : m_sessions) { + QVariantMap session = v.toMap(); + if (session.value("id").toString() == m_activeSessionId) { + return session.value("name").toString(); + } + } + return m_activeSessionId; // fallback to ID +} + +QString Ollie9pClient::activeAgentName() const +{ + if (m_activeSessionId.isEmpty() || m_agentId.isEmpty()) return QString(); + for (const QVariant &v : m_sessions) { + QVariantMap session = v.toMap(); + if (session.value("id").toString() == m_activeSessionId) { + QVariantList agents = session.value("agents").toList(); + for (const QVariant &a : agents) { + QVariantMap agent = a.toMap(); + if (agent.value("id").toString() == m_agentId) { + return agent.value("name").toString(); + } + } + } + } + return m_agentId; // fallback to ID +} + // --- Peer management --- QStringList Ollie9pClient::listPeers(const QString &sessionId, const QString &agentId) diff --git a/kde/gui/ollie9pclient.h b/kde/gui/ollie9pclient.h index 31151bd..9043832 100644 --- a/kde/gui/ollie9pclient.h +++ b/kde/gui/ollie9pclient.h @@ -22,6 +22,8 @@ class Ollie9pClient : public QObject Q_PROPERTY(QVariantList sessions READ sessions NOTIFY sessionsChanged) Q_PROPERTY(QString activeSessionId READ activeSessionId WRITE setActiveSessionId NOTIFY activeSessionIdChanged) Q_PROPERTY(QString activeAgentId READ activeAgentId NOTIFY activeAgentIdChanged) + Q_PROPERTY(QString activeSessionName READ activeSessionName NOTIFY activeSessionIdChanged) + Q_PROPERTY(QString activeAgentName READ activeAgentName NOTIFY activeAgentIdChanged) Q_PROPERTY(QString activeState READ activeState NOTIFY activeStateChanged) Q_PROPERTY(bool activeSessionPaused READ activeSessionPaused NOTIFY activeSessionPausedChanged) Q_PROPERTY(bool rootBackendsLoaded READ rootBackendsLoaded NOTIFY rootBackendsLoadedChanged) @@ -42,6 +44,8 @@ public: QString activeSessionId() const { return m_activeSessionId; } Q_INVOKABLE void setActiveSessionId(const QString &id); QString activeAgentId() const { return m_agentId; } + QString activeSessionName() const; + QString activeAgentName() const; QString activeState() const { return m_activeState; } bool activeSessionPaused() const { return m_activeSessionPaused; }