kate: windowed chat rendering for long conversations
Only render the last 40 blocks eagerly. Earlier messages are lazy-loaded in chunks of 20 as the user scrolls up. This fixes the progressive slowdown that occurred during long sessions where the full HTML re-render on every ChatUpdated signal became O(n) on the entire conversation history.
This commit is contained in:
parent
5b005c4126
commit
fad8baaa1b
|
|
@ -589,11 +589,25 @@ void ChatRenderer::render(QTextEdit *view, const QString &rawText)
|
||||||
if (m_applyStorage)
|
if (m_applyStorage)
|
||||||
m_applyStorage->clear();
|
m_applyStorage->clear();
|
||||||
|
|
||||||
QList<ChatBlock> blocks = parseBlocks(rawText);
|
m_allBlocks = parseBlocks(rawText);
|
||||||
|
|
||||||
|
// Only render the tail window
|
||||||
|
m_renderStart = qMax(0, m_allBlocks.size() - m_windowSize);
|
||||||
|
|
||||||
QString html = QStringLiteral(
|
QString html = QStringLiteral(
|
||||||
"<html><body style=\"font-family:sans-serif;font-size:medium;\">");
|
"<html><body style=\"font-family:sans-serif;font-size:medium;\">");
|
||||||
for (const auto &block : blocks)
|
|
||||||
html += blockToHtml(block);
|
// Indicate truncation if there are older blocks
|
||||||
|
if (m_renderStart > 0) {
|
||||||
|
html += QStringLiteral(
|
||||||
|
"<div id=\"ollie-load-more\" style=\"text-align:center;padding:8px;"
|
||||||
|
"color:gray;font-size:small;font-style:italic;\">↑ "
|
||||||
|
"Scroll up to load earlier messages (%1 more) ↑</div>")
|
||||||
|
.arg(m_renderStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = m_renderStart; i < m_allBlocks.size(); ++i)
|
||||||
|
html += blockToHtml(m_allBlocks[i]);
|
||||||
html += QStringLiteral("</body></html>");
|
html += QStringLiteral("</body></html>");
|
||||||
|
|
||||||
auto *sb = view->verticalScrollBar();
|
auto *sb = view->verticalScrollBar();
|
||||||
|
|
@ -619,7 +633,7 @@ void ChatRenderer::render(QTextEdit *view, const QString &rawText)
|
||||||
sb->blockSignals(false);
|
sb->blockSignals(false);
|
||||||
|
|
||||||
m_lastRawText = rawText;
|
m_lastRawText = rawText;
|
||||||
m_lastBlockCount = blocks.size();
|
m_lastBlockCount = m_allBlocks.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatRenderer::renderIncremental(QTextEdit *view, const QString &rawText)
|
bool ChatRenderer::renderIncremental(QTextEdit *view, const QString &rawText)
|
||||||
|
|
@ -636,3 +650,49 @@ bool ChatRenderer::renderIncremental(QTextEdit *view, const QString &rawText)
|
||||||
render(view, rawText);
|
render(view, rawText);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ChatRenderer::prependOlder(QTextEdit *view, int count)
|
||||||
|
{
|
||||||
|
if (m_renderStart <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
detectTheme();
|
||||||
|
|
||||||
|
int loadFrom = qMax(0, m_renderStart - count);
|
||||||
|
int loaded = m_renderStart - loadFrom;
|
||||||
|
if (loaded <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
m_renderStart = loadFrom;
|
||||||
|
|
||||||
|
// Re-render all currently-visible blocks (now including the older ones)
|
||||||
|
auto *sb = view->verticalScrollBar();
|
||||||
|
int oldMax = sb->maximum();
|
||||||
|
int oldVal = sb->value();
|
||||||
|
|
||||||
|
sb->blockSignals(true);
|
||||||
|
|
||||||
|
QString html = QStringLiteral(
|
||||||
|
"<html><body style=\"font-family:sans-serif;font-size:medium;\">");
|
||||||
|
if (m_renderStart > 0) {
|
||||||
|
html += QStringLiteral(
|
||||||
|
"<div id=\"ollie-load-more\" style=\"text-align:center;padding:8px;"
|
||||||
|
"color:gray;font-size:small;font-style:italic;\">↑ "
|
||||||
|
"Scroll up to load earlier messages (%1 more) ↑</div>")
|
||||||
|
.arg(m_renderStart);
|
||||||
|
}
|
||||||
|
for (int i = m_renderStart; i < m_allBlocks.size(); ++i)
|
||||||
|
html += blockToHtml(m_allBlocks[i]);
|
||||||
|
html += QStringLiteral("</body></html>");
|
||||||
|
|
||||||
|
view->setHtml(html);
|
||||||
|
|
||||||
|
// Restore scroll position offset by the added content height
|
||||||
|
int newMax = sb->maximum();
|
||||||
|
int addedHeight = newMax - oldMax;
|
||||||
|
sb->setValue(oldVal + addedHeight);
|
||||||
|
|
||||||
|
sb->blockSignals(false);
|
||||||
|
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ struct SubBlock {
|
||||||
/**
|
/**
|
||||||
* ChatRenderer parses ollie chat output into structured blocks
|
* ChatRenderer parses ollie chat output into structured blocks
|
||||||
* and renders them as styled HTML in a QTextEdit.
|
* and renders them as styled HTML in a QTextEdit.
|
||||||
|
*
|
||||||
|
* To avoid performance degradation on long conversations, only a
|
||||||
|
* tail window of blocks is eagerly rendered. Earlier blocks are
|
||||||
|
* loaded on demand when the user scrolls up.
|
||||||
*/
|
*/
|
||||||
class ChatRenderer
|
class ChatRenderer
|
||||||
{
|
{
|
||||||
|
|
@ -62,13 +66,19 @@ public:
|
||||||
};
|
};
|
||||||
void setApplyStorage(QList<ApplyBlock> *storage) { m_applyStorage = storage; }
|
void setApplyStorage(QList<ApplyBlock> *storage) { m_applyStorage = storage; }
|
||||||
|
|
||||||
|
/// Maximum number of blocks to render eagerly (tail window).
|
||||||
|
static constexpr int DefaultWindowSize = 40;
|
||||||
|
void setWindowSize(int n) { m_windowSize = n; }
|
||||||
|
int windowSize() const { return m_windowSize; }
|
||||||
|
|
||||||
/// Parse raw chat text into blocks
|
/// Parse raw chat text into blocks
|
||||||
QList<ChatBlock> parseBlocks(const QString &text) const;
|
QList<ChatBlock> parseBlocks(const QString &text) const;
|
||||||
|
|
||||||
/// Parse a block's content into sub-blocks (prose, code, diff)
|
/// Parse a block's content into sub-blocks (prose, code, diff)
|
||||||
QList<SubBlock> parseSubBlocks(const QString &content) const;
|
QList<SubBlock> parseSubBlocks(const QString &content) const;
|
||||||
|
|
||||||
/// Render full chat text into the QTextEdit as styled HTML
|
/// Render the tail window of blocks into the QTextEdit.
|
||||||
|
/// Parses all blocks but only generates HTML for the last windowSize().
|
||||||
void render(QTextEdit *view, const QString &rawText);
|
void render(QTextEdit *view, const QString &rawText);
|
||||||
|
|
||||||
/// Render incrementally: assumes rawText is the full text and only
|
/// Render incrementally: assumes rawText is the full text and only
|
||||||
|
|
@ -76,6 +86,19 @@ public:
|
||||||
/// an incremental update, false if it did a full re-render.
|
/// an incremental update, false if it did a full re-render.
|
||||||
bool renderIncremental(QTextEdit *view, const QString &rawText);
|
bool renderIncremental(QTextEdit *view, const QString &rawText);
|
||||||
|
|
||||||
|
/// Prepend older blocks into the view. Returns the number of
|
||||||
|
/// blocks actually prepended (0 if all blocks are already shown).
|
||||||
|
int prependOlder(QTextEdit *view, int count);
|
||||||
|
|
||||||
|
/// True if there are older blocks not yet rendered.
|
||||||
|
bool hasOlderBlocks() const { return m_renderStart > 0; }
|
||||||
|
|
||||||
|
/// Total number of parsed blocks.
|
||||||
|
int totalBlocks() const { return m_allBlocks.size(); }
|
||||||
|
|
||||||
|
/// Number of blocks currently rendered.
|
||||||
|
int renderedBlocks() const { return m_allBlocks.size() - m_renderStart; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString blockToHtml(const ChatBlock &block) const;
|
QString blockToHtml(const ChatBlock &block) const;
|
||||||
QString subBlockToHtml(const SubBlock &sub, bool preserveWhitespace = false) const;
|
QString subBlockToHtml(const SubBlock &sub, bool preserveWhitespace = false) const;
|
||||||
|
|
@ -90,6 +113,11 @@ private:
|
||||||
QString m_lastRawText; // for incremental rendering
|
QString m_lastRawText; // for incremental rendering
|
||||||
int m_lastBlockCount = 0;
|
int m_lastBlockCount = 0;
|
||||||
|
|
||||||
|
// Windowed rendering state
|
||||||
|
QList<ChatBlock> m_allBlocks; // all parsed blocks
|
||||||
|
int m_renderStart = 0; // index of first rendered block
|
||||||
|
int m_windowSize = DefaultWindowSize;
|
||||||
|
|
||||||
// Colors (derived from palette)
|
// Colors (derived from palette)
|
||||||
mutable bool m_dark = false;
|
mutable bool m_dark = false;
|
||||||
void detectTheme() const;
|
void detectTheme() const;
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
||||||
m_chatView->installEventFilter(this); // handle resize in eventFilter
|
m_chatView->installEventFilter(this); // handle resize in eventFilter
|
||||||
repositionBtn();
|
repositionBtn();
|
||||||
|
|
||||||
// Also show/hide on manual scroll
|
// Also show/hide on manual scroll + lazy-load older messages
|
||||||
connect(m_chatView->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
|
connect(m_chatView->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) {
|
||||||
auto *sb = m_chatView->verticalScrollBar();
|
auto *sb = m_chatView->verticalScrollBar();
|
||||||
if (value >= sb->maximum() - 4) {
|
if (value >= sb->maximum() - 4) {
|
||||||
|
|
@ -395,6 +395,13 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
||||||
} else if (!m_chatView->toPlainText().isEmpty()) {
|
} else if (!m_chatView->toPlainText().isEmpty()) {
|
||||||
m_scrollBottomBtn->show();
|
m_scrollBottomBtn->show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lazy-load: when scrolled near the top, prepend older blocks
|
||||||
|
if (value <= 50 && m_renderer->hasOlderBlocks() && !m_loadingOlder) {
|
||||||
|
m_loadingOlder = true;
|
||||||
|
m_renderer->prependOlder(m_chatView, 20);
|
||||||
|
m_loadingOlder = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prompt input
|
// Prompt input
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ private:
|
||||||
void annotateFileReferences(const QString &chatText);
|
void annotateFileReferences(const QString &chatText);
|
||||||
|
|
||||||
QList<ChatRenderer::ApplyBlock> m_applyBlocks;
|
QList<ChatRenderer::ApplyBlock> m_applyBlocks;
|
||||||
|
bool m_loadingOlder = false; // guard against recursive scroll-triggered loads
|
||||||
|
|
||||||
// Inline annotations (MovingRanges highlighting lines the agent references)
|
// Inline annotations (MovingRanges highlighting lines the agent references)
|
||||||
QList<KTextEditor::MovingRange *> m_annotations;
|
QList<KTextEditor::MovingRange *> m_annotations;
|
||||||
|
|
|
||||||
Reference in New Issue