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