diff --git a/kate/chat_renderer.cpp b/kate/chat_renderer.cpp index fa552fc..b3de0b9 100644 --- a/kate/chat_renderer.cpp +++ b/kate/chat_renderer.cpp @@ -589,11 +589,25 @@ void ChatRenderer::render(QTextEdit *view, const QString &rawText) if (m_applyStorage) m_applyStorage->clear(); - QList blocks = parseBlocks(rawText); + m_allBlocks = parseBlocks(rawText); + + // Only render the tail window + m_renderStart = qMax(0, m_allBlocks.size() - m_windowSize); + QString html = QStringLiteral( ""); - for (const auto &block : blocks) - html += blockToHtml(block); + + // Indicate truncation if there are older blocks + if (m_renderStart > 0) { + html += QStringLiteral( + "
↑ " + "Scroll up to load earlier messages (%1 more) ↑
") + .arg(m_renderStart); + } + + for (int i = m_renderStart; i < m_allBlocks.size(); ++i) + html += blockToHtml(m_allBlocks[i]); html += QStringLiteral(""); 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( + ""); + if (m_renderStart > 0) { + html += QStringLiteral( + "
↑ " + "Scroll up to load earlier messages (%1 more) ↑
") + .arg(m_renderStart); + } + for (int i = m_renderStart; i < m_allBlocks.size(); ++i) + html += blockToHtml(m_allBlocks[i]); + html += QStringLiteral(""); + + 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; +} diff --git a/kate/chat_renderer.h b/kate/chat_renderer.h index 03bfc61..189b1a6 100644 --- a/kate/chat_renderer.h +++ b/kate/chat_renderer.h @@ -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 *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 parseBlocks(const QString &text) const; /// Parse a block's content into sub-blocks (prose, code, diff) QList 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 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; diff --git a/kate/ollie_kate.cpp b/kate/ollie_kate.cpp index 7d92b64..f393312 100644 --- a/kate/ollie_kate.cpp +++ b/kate/ollie_kate.cpp @@ -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 diff --git a/kate/ollie_kate.h b/kate/ollie_kate.h index 85f8726..8836a85 100644 --- a/kate/ollie_kate.h +++ b/kate/ollie_kate.h @@ -146,6 +146,7 @@ private: void annotateFileReferences(const QString &chatText); QList m_applyBlocks; + bool m_loadingOlder = false; // guard against recursive scroll-triggered loads // Inline annotations (MovingRanges highlighting lines the agent references) QList m_annotations;