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.
This commit is contained in:
Levi Neely 2026-07-18 17:36:36 +02:00
parent e021724c52
commit 7ccce95445
2 changed files with 54 additions and 4 deletions

View File

@ -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<ChatBlock> 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()) {

View File

@ -74,6 +74,8 @@ signals:
private:
void reparse();
void reparseIncremental();
void updateVisibleWindow();
QList<ChatBlock> parseBlocks(const QString &text) const;
QVariantList parseSubBlocks(const QString &content) const;
@ -81,6 +83,7 @@ private:
QList<ChatBlock> m_allBlocks; // all parsed blocks
QList<ChatBlock> 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<QString, QString> m_highlightCache;
};