gui: add bookmark feature for chat blocks

- Add bookmark button (📌) on chat block hover
- Bookmarks section in left panel (SessionTree) with grouped view
- Non-persistent GUI-only state using ListModel
- Click bookmark to scroll to that block in chat
- Bookmark format: sessionName/agentName with block content preview
- Add activeSessionName/activeAgentName C++ getters for display
This commit is contained in:
Levi Neely 2026-10-09 13:21:33 +02:00
parent c8897b8251
commit 96b6406871
6 changed files with 226 additions and 0 deletions

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -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)

View File

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