From f122a69bcef4d0fe5007203ff443236830d1ad15 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Thu, 16 Jul 2026 06:46:47 +0200 Subject: [PATCH] reverse-scroll chat: start at bottom, lazy-load on scroll up - Model shows last 40 blocks initially, positioned at end - Scrolling near top triggers loadEarlier() which prepends 30 blocks - Scroll position maintained after prepend via contentY adjustment - All blocks still parsed (for searching later) but only tail rendered - Reduced initial render cost significantly --- gui/chatblockmodel.cpp | 48 +++++++++++++++++++++++++++++++++--------- gui/chatblockmodel.h | 7 ++++-- gui/main.qml | 16 ++++++++++++++ 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp index 4f8bf2f..c28199b 100644 --- a/gui/chatblockmodel.cpp +++ b/gui/chatblockmodel.cpp @@ -81,25 +81,33 @@ void ChatBlockModel::setRawText(const QString &text) emit rawTextChanged(); } -static constexpr int MAX_VISIBLE_BLOCKS = 80; +static constexpr int INITIAL_BLOCKS = 40; +static constexpr int LOAD_MORE_COUNT = 30; void ChatBlockModel::reparse() { - QList allBlocks = parseBlocks(m_rawText); + m_allBlocks = parseBlocks(m_rawText); - // Only keep the last N blocks to avoid rendering hundreds of delegates - QList newBlocks; - if (allBlocks.size() > MAX_VISIBLE_BLOCKS) { - newBlocks = allBlocks.mid(allBlocks.size() - MAX_VISIBLE_BLOCKS); + // Determine visible window: last INITIAL_BLOCKS (or expand if already showing more) + int desiredStart; + if (m_blocks.isEmpty()) { + // Initial load: show last N blocks + desiredStart = qMax(0, m_allBlocks.size() - INITIAL_BLOCKS); } else { - newBlocks = allBlocks; + // Streaming update: keep the same start offset, just extend the tail + desiredStart = m_visibleStart; + // But if allBlocks shrank (session switch), reset + if (desiredStart >= m_allBlocks.size()) + desiredStart = qMax(0, m_allBlocks.size() - INITIAL_BLOCKS); } + m_visibleStart = desiredStart; + + QList newBlocks = m_allBlocks.mid(m_visibleStart); // Incremental update: only change what differs int commonCount = qMin(m_blocks.size(), newBlocks.size()); - int firstDiff = commonCount; // index of first different block + int firstDiff = commonCount; - // Find first block that changed (usually the last one during streaming) for (int i = 0; i < commonCount; ++i) { if (m_blocks[i].content != newBlocks[i].content || m_blocks[i].type != newBlocks[i].type || @@ -122,7 +130,7 @@ void ChatBlockModel::reparse() emit dataChanged(index(i), index(i)); } - // Append new blocks + // Append new blocks at end if (newBlocks.size() > m_blocks.size()) { beginInsertRows(QModelIndex(), m_blocks.size(), newBlocks.size() - 1); for (int i = m_blocks.size(); i < newBlocks.size(); ++i) { @@ -132,6 +140,26 @@ void ChatBlockModel::reparse() } } +bool ChatBlockModel::loadEarlier() +{ + if (m_visibleStart <= 0) + return false; // already showing everything + + int loadCount = qMin(LOAD_MORE_COUNT, m_visibleStart); + int newStart = m_visibleStart - loadCount; + + QList prepend = m_allBlocks.mid(newStart, loadCount); + + beginInsertRows(QModelIndex(), 0, loadCount - 1); + for (int i = loadCount - 1; i >= 0; --i) { + m_blocks.prepend(prepend[i]); + } + m_visibleStart = newStart; + endInsertRows(); + + return true; +} + QList ChatBlockModel::parseBlocks(const QString &text) const { QList blocks; diff --git a/gui/chatblockmodel.h b/gui/chatblockmodel.h index b1c9dac..bea9821 100644 --- a/gui/chatblockmodel.h +++ b/gui/chatblockmodel.h @@ -61,6 +61,7 @@ public: Q_INVOKABLE QString highlightedHtml(const QString &code, const QString &language) const; Q_INVOKABLE void copyToClipboard(const QString &text) const; + Q_INVOKABLE bool loadEarlier(); // returns true if more blocks were loaded signals: void rawTextChanged(); @@ -71,8 +72,10 @@ private: QVariantList parseSubBlocks(const QString &content) const; QString m_rawText; - QList m_blocks; - mutable QHash m_highlightCache; // key: code+lang, value: html + QList m_allBlocks; // all parsed blocks + QList m_blocks; // visible window (tail of m_allBlocks) + int m_visibleStart = 0; // index into m_allBlocks where visible window starts + mutable QHash m_highlightCache; }; #endif // CHAT_BLOCK_MODEL_H diff --git a/gui/main.qml b/gui/main.qml index fc24f55..9f35d3c 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -371,13 +371,29 @@ ApplicationWindow { spacing: 8 cacheBuffer: 200 model: chatModel + verticalLayoutDirection: ListView.TopToBottom property bool _scrolling: false + Component.onCompleted: { + Qt.callLater(function() { chatList.positionViewAtEnd() }) + } + onContentYChanged: { if (_scrolling) return let bottom = contentHeight - height parent.atBottom = (bottom <= 0 || contentY >= bottom - 4) + + // Load earlier messages when scrolled near top + if (contentY < 100 && contentHeight > height) { + let oldHeight = contentHeight + if (chatModel.loadEarlier()) { + // Maintain scroll position after prepend + Qt.callLater(function() { + chatList.contentY += (chatList.contentHeight - oldHeight) + }) + } + } } onContentHeightChanged: { if (parent.atBottom && !_scrolling) {