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