From 7ccce9544568dd1f7a56a89e9ad878e863a4b4bd Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:36:36 +0200 Subject: [PATCH] gui: incremental chat parsing during streaming reparseIncremental() only re-parses from the last block boundary instead of the entire chat text. Fixes UI lag and typing blockage during long streaming responses. Also extracted updateVisibleWindow() for reuse. --- gui/chatblockmodel.cpp | 55 +++++++++++++++++++++++++++++++++++++++--- gui/chatblockmodel.h | 3 +++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/gui/chatblockmodel.cpp b/gui/chatblockmodel.cpp index 68372bb..126abb4 100644 --- a/gui/chatblockmodel.cpp +++ b/gui/chatblockmodel.cpp @@ -107,17 +107,16 @@ void ChatBlockModel::setRawText(const QString &text) { if (text == m_rawText) return; - // If text is just appended (common during streaming), only reparse the last block + // If text is just appended (common during streaming), only reparse from the last block bool isAppend = text.startsWith(m_rawText) && !m_rawText.isEmpty(); m_rawText = text; if (isAppend && !m_allBlocks.isEmpty()) { - // Re-parse only: find where the last block started in the text and reparse from there - // Simpler: just reparse the full text but it's cached via incremental diff - reparse(); + reparseIncremental(); } else { // Full reparse (session switch, initial load) m_allBlocks.clear(); + m_lastBlockTextOffset = 0; beginResetModel(); m_blocks.clear(); m_visibleStart = 0; @@ -130,10 +129,58 @@ void ChatBlockModel::setRawText(const QString &text) static constexpr int INITIAL_BLOCKS = 20; static constexpr int LOAD_MORE_COUNT = 30; +void ChatBlockModel::reparseIncremental() +{ + // Only re-parse from where the last block started — O(tail) not O(full text) + QString tail = m_rawText.mid(m_lastBlockTextOffset); + QList tailBlocks = parseBlocks(tail); + + // Replace the last block (which may have grown) and append any new blocks + if (!m_allBlocks.isEmpty()) + m_allBlocks.removeLast(); + m_allBlocks.append(tailBlocks); + + // Update offset: last block starts at the last header in the tail + if (tailBlocks.size() > 1) { + // Multiple blocks parsed from tail — advance offset to where the last one starts + // Find the last header boundary in m_rawText + static const QRegularExpression headerRe( + R"(^\[(user|assistant|reasoning|call|tool|error|info)(?::([^\]]*))\])", + QRegularExpression::MultilineOption); + auto it = headerRe.globalMatch(m_rawText); + int lastPos = 0; + while (it.hasNext()) { + auto match = it.next(); + lastPos = match.capturedStart(); + } + m_lastBlockTextOffset = lastPos; + } + + updateVisibleWindow(); +} + void ChatBlockModel::reparse() { m_allBlocks = parseBlocks(m_rawText); + // Track where the last block starts for future incremental parses + if (!m_allBlocks.isEmpty()) { + static const QRegularExpression headerRe( + R"(^\[(user|assistant|reasoning|call|tool|error|info)(?::([^\]]*))\])", + QRegularExpression::MultilineOption); + auto it = headerRe.globalMatch(m_rawText); + int lastPos = 0; + while (it.hasNext()) { + auto match = it.next(); + lastPos = match.capturedStart(); + } + m_lastBlockTextOffset = lastPos; + } + updateVisibleWindow(); +} + +void ChatBlockModel::updateVisibleWindow() +{ // Determine visible window: last INITIAL_BLOCKS (or expand if already showing more) int desiredStart; if (m_blocks.isEmpty()) { diff --git a/gui/chatblockmodel.h b/gui/chatblockmodel.h index 73ba5ff..6f3db54 100644 --- a/gui/chatblockmodel.h +++ b/gui/chatblockmodel.h @@ -74,6 +74,8 @@ signals: private: void reparse(); + void reparseIncremental(); + void updateVisibleWindow(); QList parseBlocks(const QString &text) const; QVariantList parseSubBlocks(const QString &content) const; @@ -81,6 +83,7 @@ private: 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 + int m_lastBlockTextOffset = 0; // char offset in m_rawText where last block starts mutable QHash m_highlightCache; };