gui: use stable blockId instead of index for bookmarks/plumb
- Add blockId field to ChatBlock (12-char MD5 hash of counter+name) - Bookmarks now use blockId instead of volatile list index - Plumb URLs use blockId: ollie://session/agent#abc123def456 - Add indexForBlockId() to look up current index from stable ID - Action buttons always visible (no hover flicker)
This commit is contained in:
parent
483a4f0d4d
commit
2f45509af3
|
|
@ -18,15 +18,13 @@ Column {
|
|||
required property var blockSegments
|
||||
required property string blockRenderedHtml
|
||||
required property int index
|
||||
|
||||
// Block index for bookmarking
|
||||
property int blockIndex: index
|
||||
required property string blockId
|
||||
|
||||
// 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)
|
||||
property var addBookmark: function(sid, sn, aid, an, bid, bt, bn, p) { return false }
|
||||
property var removeBookmark: function(sid, aid, bid) { return false }
|
||||
property var checkBookmarked: function(sid, aid, bid) { return false }
|
||||
property bool isBookmarked: checkBookmarked(ollie.activeSessionId, ollie.activeAgentId, blockId)
|
||||
|
||||
// Expand state based on block type
|
||||
property bool blockExpanded: {
|
||||
|
|
@ -130,11 +128,9 @@ Column {
|
|||
opacity: 0.9
|
||||
}
|
||||
|
||||
// Action buttons (visible on hover) — Acme-style tag line
|
||||
// Action buttons
|
||||
Row {
|
||||
spacing: 2
|
||||
visible: root.hovered
|
||||
opacity: 0.8
|
||||
|
||||
// Copy block content
|
||||
ToolButton {
|
||||
|
|
@ -200,7 +196,7 @@ Column {
|
|||
ToolTip.text: root.isBookmarked ? "Remove bookmark" : "Bookmark"
|
||||
onClicked: {
|
||||
if (root.isBookmarked) {
|
||||
root.removeBookmark(ollie.activeSessionId, ollie.activeAgentId, root.blockIndex)
|
||||
root.removeBookmark(ollie.activeSessionId, ollie.activeAgentId, root.blockId)
|
||||
} else {
|
||||
var sessionName = ollie.activeSessionName || ollie.activeSessionId
|
||||
var agentName = ollie.activeAgentName || ollie.activeAgentId
|
||||
|
|
@ -210,7 +206,7 @@ Column {
|
|||
sessionName,
|
||||
ollie.activeAgentId,
|
||||
agentName,
|
||||
root.blockIndex,
|
||||
root.blockId,
|
||||
blockType,
|
||||
blockName,
|
||||
preview
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ Item {
|
|||
chatView.positionViewAtIndex(blockIndex, ListView.Center)
|
||||
}
|
||||
}
|
||||
function scrollToBlockId(blockId) {
|
||||
var idx = chatModel.indexForBlockId(blockId)
|
||||
if (idx >= 0) {
|
||||
chatView.positionViewAtIndex(idx, ListView.Center)
|
||||
}
|
||||
}
|
||||
function trimVisibleBlocks() {
|
||||
if (chatModel.count > chatPane.maxVisibleBlocks)
|
||||
chatModel.trimFront(chatPane.maxVisibleBlocks)
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ Item {
|
|||
width: bookmarksList.width
|
||||
height: theme.fontSize > 0 ? theme.fontSize * 3 : 36
|
||||
|
||||
property string ollieUrl: "ollie://" + encodeURIComponent(sessionName) + "/" + encodeURIComponent(agentName) + "#" + blockIndex
|
||||
property string ollieUrl: "ollie://" + encodeURIComponent(sessionName) + "/" + encodeURIComponent(agentName) + "#" + blockId
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
|
@ -464,7 +464,7 @@ Item {
|
|||
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)
|
||||
onClicked: root.removeBookmark(sessionId, agentId, blockId)
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.delay: 500
|
||||
ToolTip.text: "Remove bookmark"
|
||||
|
|
@ -474,8 +474,7 @@ Item {
|
|||
onClicked: {
|
||||
// Switch to the session/agent and scroll to block
|
||||
ollie.switchAgent(sessionId, agentId)
|
||||
// Signal to ChatPane to scroll to blockIndex
|
||||
chatPane.scrollToBlock(blockIndex)
|
||||
chatPane.scrollToBlockId(blockId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ QVariant ChatBlockModel::data(const QModelIndex &index, int role) const
|
|||
return b.renderedHtml;
|
||||
case SegmentsRole:
|
||||
return segmentsToVariant(b.segments);
|
||||
case BlockIdRole:
|
||||
return b.blockId;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
@ -191,6 +193,7 @@ QHash<int, QByteArray> ChatBlockModel::roleNames() const
|
|||
{ContentRole, "blockContent"},
|
||||
{RenderedHtmlRole, "blockRenderedHtml"},
|
||||
{SegmentsRole, "blockSegments"},
|
||||
{BlockIdRole, "blockId"},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -258,6 +261,15 @@ void ChatBlockModel::trimFront(int keepCount)
|
|||
emit countChanged();
|
||||
}
|
||||
|
||||
int ChatBlockModel::indexForBlockId(const QString &blockId) const
|
||||
{
|
||||
for (int i = 0; i < m_blocks.size(); ++i) {
|
||||
if (m_blocks[i].blockId == blockId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString ChatBlockModel::getDefaultTextColor() const
|
||||
{
|
||||
const auto textColor = QApplication::palette().color(QPalette::Text);
|
||||
|
|
@ -808,6 +820,11 @@ void ChatBlockModel::processLine(const QString &line)
|
|||
ChatBlock newBlock;
|
||||
newBlock.type = blockTypeFromString(blockName.section(':', 0, 0));
|
||||
newBlock.name = blockName.section(':', 1);
|
||||
// Generate stable block ID: sequential counter + type + name hash
|
||||
// The counter ensures uniqueness even for identical type/name pairs
|
||||
QString idSource = QString::number(m_blockIdCounter++) + blockName;
|
||||
newBlock.blockId = QString::fromLatin1(
|
||||
QCryptographicHash::hash(idSource.toUtf8(), QCryptographicHash::Md5).toHex().left(12));
|
||||
m_blocks.append(newBlock);
|
||||
endInsertRows();
|
||||
emit countChanged();
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ struct ChatBlock {
|
|||
QString renderedHtml; // Legacy: full HTML (kept for compatibility)
|
||||
QList<BlockSegment> segments; // Structured segments for interactive rendering
|
||||
BlockRenderState renderState; // Incremental rendering state
|
||||
QString blockId; // Stable identifier (hash of type+name+first N chars of content)
|
||||
};
|
||||
|
||||
class ChatBlockModel : public QAbstractListModel
|
||||
|
|
@ -85,6 +86,7 @@ public:
|
|||
ContentRole,
|
||||
RenderedHtmlRole,
|
||||
SegmentsRole, // QVariantList of segment maps
|
||||
BlockIdRole, // Stable block identifier
|
||||
};
|
||||
|
||||
explicit ChatBlockModel(QObject *parent = nullptr);
|
||||
|
|
@ -97,6 +99,7 @@ public:
|
|||
Q_INVOKABLE void appendData(const QString &text);
|
||||
Q_INVOKABLE void reset();
|
||||
Q_INVOKABLE void trimFront(int keepCount);
|
||||
Q_INVOKABLE int indexForBlockId(const QString &blockId) const;
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
|
@ -138,6 +141,7 @@ private:
|
|||
|
||||
QList<ChatBlock> m_blocks;
|
||||
QString m_lineBuffer;
|
||||
quint64 m_blockIdCounter = 0; // Monotonic counter for stable block IDs
|
||||
|
||||
// Context blocks are injected into prompts but hidden from chat rendering.
|
||||
bool m_inContext = false;
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ ApplicationWindow {
|
|||
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 }
|
||||
// Each entry: { sessionId, sessionName, agentId, agentName, blockId, blockType, blockName, preview }
|
||||
ListModel {
|
||||
id: bookmarksModel
|
||||
}
|
||||
|
||||
function addBookmark(sessionId, sessionName, agentId, agentName, blockIndex, blockType, blockName, preview) {
|
||||
function addBookmark(sessionId, sessionName, agentId, agentName, blockId, 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) {
|
||||
if (b.sessionId === sessionId && b.agentId === agentId && b.blockId === blockId) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ ApplicationWindow {
|
|||
sessionName: sessionName,
|
||||
agentId: agentId,
|
||||
agentName: agentName,
|
||||
blockIndex: blockIndex,
|
||||
blockId: blockId,
|
||||
blockType: blockType,
|
||||
blockName: blockName,
|
||||
preview: preview.substring(0, 80) + (preview.length > 80 ? "…" : "")
|
||||
|
|
@ -40,10 +40,10 @@ ApplicationWindow {
|
|||
return true
|
||||
}
|
||||
|
||||
function removeBookmark(sessionId, agentId, blockIndex) {
|
||||
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.blockIndex === blockIndex) {
|
||||
if (b.sessionId === sessionId && b.agentId === agentId && b.blockId === blockId) {
|
||||
bookmarksModel.remove(i)
|
||||
return true
|
||||
}
|
||||
|
|
@ -51,10 +51,10 @@ ApplicationWindow {
|
|||
return false
|
||||
}
|
||||
|
||||
function isBookmarked(sessionId, agentId, blockIndex) {
|
||||
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.blockIndex === blockIndex) {
|
||||
if (b.sessionId === sessionId && b.agentId === agentId && b.blockId === blockId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -62,17 +62,17 @@ ApplicationWindow {
|
|||
}
|
||||
|
||||
// Handle plumb messages from the ollie port
|
||||
// URL format: ollie://session/agent#block
|
||||
// URL format: ollie://session/agent#blockId
|
||||
function handleOllieMessage(url) {
|
||||
// Parse ollie://session/agent#block
|
||||
var match = url.match(/^ollie:\/\/([^\/]+)(?:\/([^#]+))?(?:#(\d+))?$/)
|
||||
// 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 blockIndex = match[3] ? parseInt(match[3]) : -1
|
||||
var blockId = match[3] || ""
|
||||
|
||||
// Find session by name and switch to it
|
||||
var sessions = ollie.sessions
|
||||
|
|
@ -92,10 +92,10 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
// If block specified, scroll to it after a short delay
|
||||
if (blockIndex >= 0) {
|
||||
// If blockId specified, scroll to it after a short delay
|
||||
if (blockId) {
|
||||
Qt.callLater(function() {
|
||||
chatPane.scrollToBlock(blockIndex)
|
||||
chatPane.scrollToBlockId(blockId)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue