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)
|
||||
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(
|
||||
"<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>");
|
||||
|
||||
auto *sb = view->verticalScrollBar();
|
||||
|
|
@ -619,7 +633,7 @@ void ChatRenderer::render(QTextEdit *view, const QString &rawText)
|
|||
sb->blockSignals(false);
|
||||
|
||||
m_lastRawText = rawText;
|
||||
m_lastBlockCount = blocks.size();
|
||||
m_lastBlockCount = m_allBlocks.size();
|
||||
}
|
||||
|
||||
bool ChatRenderer::renderIncremental(QTextEdit *view, const QString &rawText)
|
||||
|
|
@ -636,3 +650,49 @@ bool ChatRenderer::renderIncremental(QTextEdit *view, const QString &rawText)
|
|||
render(view, rawText);
|
||||
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
|
||||
* 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
|
||||
{
|
||||
|
|
@ -62,13 +66,19 @@ public:
|
|||
};
|
||||
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
|
||||
QList<ChatBlock> parseBlocks(const QString &text) const;
|
||||
|
||||
/// Parse a block's content into sub-blocks (prose, code, diff)
|
||||
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);
|
||||
|
||||
/// 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.
|
||||
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:
|
||||
QString blockToHtml(const ChatBlock &block) const;
|
||||
QString subBlockToHtml(const SubBlock &sub, bool preserveWhitespace = false) const;
|
||||
|
|
@ -90,6 +113,11 @@ private:
|
|||
QString m_lastRawText; // for incremental rendering
|
||||
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)
|
||||
mutable bool m_dark = false;
|
||||
void detectTheme() const;
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
m_chatView->installEventFilter(this); // handle resize in eventFilter
|
||||
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) {
|
||||
auto *sb = m_chatView->verticalScrollBar();
|
||||
if (value >= sb->maximum() - 4) {
|
||||
|
|
@ -395,6 +395,13 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
|
|||
} else if (!m_chatView->toPlainText().isEmpty()) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ private:
|
|||
void annotateFileReferences(const QString &chatText);
|
||||
|
||||
QList<ChatRenderer::ApplyBlock> m_applyBlocks;
|
||||
bool m_loadingOlder = false; // guard against recursive scroll-triggered loads
|
||||
|
||||
// Inline annotations (MovingRanges highlighting lines the agent references)
|
||||
QList<KTextEditor::MovingRange *> m_annotations;
|
||||
|
|
|
|||
Reference in New Issue