#ifndef OLLIE_CHAT_RENDERER_H #define OLLIE_CHAT_RENDERER_H #include #include #include #include #include namespace KSyntaxHighlighting { class Repository; } // Parsed chat block struct ChatBlock { enum Type { User, Assistant, Reasoning, Call, Tool, Error, Info }; Type type; QString name; // tool/call name QString content; // raw text content }; // Sub-block within a message (prose, code fence, diff) struct SubBlock { enum Type { Prose, Code, Diff, Interrupt }; Type type; QString language; QString fenceMeta; // extra text after language on the fence line (e.g., "/path:10-20") QString content; }; /** * 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 { public: ChatRenderer(); ~ChatRenderer(); /// Set external diff storage. When set, diffs are stored here and /// rendered as clickable links rather than inline. void setDiffStorage(QStringList *storage) { m_diffStorage = storage; } /// Storage for apply-able code blocks. Each entry is the code content. /// Rendered with a clickable "Apply" link. struct ApplyBlock { QString filePath; QString code; int startLine = -1; // 1-indexed, inclusive. -1 = insert at cursor int endLine = -1; // 1-indexed, inclusive. -1 = insert at cursor }; 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 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 /// the tail has changed since last render. Returns true if it did /// 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; QString highlightCode(const QString &code, const QString &language) const; QString renderDiff(const QString &diff) const; QString renderProse(const QString &prose) const; QString escapeHtml(const QString &text) const; KSyntaxHighlighting::Repository *m_repo; QStringList *m_diffStorage = nullptr; QList *m_applyStorage = nullptr; 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; }; #endif // OLLIE_CHAT_RENDERER_H