kate: extract chat_renderer, simplify kate plugin

This commit is contained in:
Levi Neely 2026-07-16 08:57:55 +02:00
parent 59285cda9f
commit d3c8aece38
5 changed files with 586 additions and 35 deletions

View File

@ -24,6 +24,13 @@ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
if(OLLIE_KF5)
# --- Qt5 / KF5 ---
find_package(Qt5 REQUIRED COMPONENTS Core DBus Widgets Quick Qml QuickControls2)
# Override KDE_INSTALL_PLUGINDIR so kcoreaddons_add_plugin installs to the
# Qt5 plugin directory (e.g. /usr/lib/x86_64-linux-gnu/qt5/plugins).
get_target_property(_QT5_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
execute_process(COMMAND "${_QT5_QMAKE_EXECUTABLE}" -query QT_INSTALL_PLUGINS
OUTPUT_VARIABLE _QT5_PLUGIN_DIR OUTPUT_STRIP_TRAILING_WHITESPACE)
set(KDE_INSTALL_PLUGINDIR "${_QT5_PLUGIN_DIR}" CACHE PATH "" FORCE)
find_package(KF5 REQUIRED COMPONENTS
Notifications
Runner
@ -59,9 +66,10 @@ if(OLLIE_KF5)
)
# --- Kate Plugin ---
kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp INSTALL_NAMESPACE "ktexteditor")
kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp kate/chat_renderer.cpp INSTALL_NAMESPACE "ktexteditor")
target_link_libraries(ollie_kate
KF5::TextEditor
KF5::SyntaxHighlighting
Qt5::Core
Qt5::DBus
Qt5::Widgets
@ -159,9 +167,11 @@ else()
# --- Kate Plugin ---
find_package(KF6TextEditor REQUIRED)
kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp INSTALL_NAMESPACE "ktexteditor")
find_package(KF6SyntaxHighlighting REQUIRED)
kcoreaddons_add_plugin(ollie_kate SOURCES kate/ollie_kate.cpp kate/ollie_ghost.cpp kate/chat_renderer.cpp INSTALL_NAMESPACE "ktexteditor")
target_link_libraries(ollie_kate
KF6::TextEditor
KF6::SyntaxHighlighting
Qt6::Core
Qt6::DBus
Qt6::Widgets
@ -189,7 +199,6 @@ else()
plasma_install_package(plasmoid org.ollie.plasmoid)
# --- GUI App ---
find_package(KF6SyntaxHighlighting REQUIRED)
add_executable(ollie-gui
gui/main.cpp
gui/thememanager.cpp

470
kate/chat_renderer.cpp Normal file
View File

@ -0,0 +1,470 @@
#include "chat_renderer.h"
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Theme>
#include <KSyntaxHighlighting/AbstractHighlighter>
#include <KSyntaxHighlighting/State>
#include <KSyntaxHighlighting/Format>
#include <QApplication>
#include <QPalette>
#include <QTextCursor>
#include <QScrollBar>
#include <QRegularExpression>
// --- HTML Highlighter ---
class HtmlHighlighter : public KSyntaxHighlighting::AbstractHighlighter
{
public:
QString html;
using AbstractHighlighter::AbstractHighlighter;
KSyntaxHighlighting::State processLine(const QString &line, const KSyntaxHighlighting::State &state)
{
m_currentLine = line;
return highlightLine(line, state);
}
protected:
void applyFormat(int offset, int length, const KSyntaxHighlighting::Format &format) override
{
QString text = m_currentLine.mid(offset, length).toHtmlEscaped();
if (format.hasTextColor(theme())) {
QColor fg = format.textColor(theme());
html += QStringLiteral("<span style=\"color:") + fg.name() + QStringLiteral("\">") + text + QStringLiteral("</span>");
} else {
html += text;
}
}
private:
QString m_currentLine;
};
// --- ChatRenderer ---
ChatRenderer::ChatRenderer()
: m_repo(new KSyntaxHighlighting::Repository())
{
}
ChatRenderer::~ChatRenderer()
{
delete m_repo;
}
void ChatRenderer::detectTheme() const
{
m_dark = QApplication::palette().color(QPalette::Window).lightnessF() < 0.5;
}
QString ChatRenderer::escapeHtml(const QString &text) const
{
return text.toHtmlEscaped();
}
// --- Block Parsing ---
QList<ChatBlock> ChatRenderer::parseBlocks(const QString &text) const
{
QList<ChatBlock> blocks;
if (text.isEmpty()) return blocks;
static const QRegularExpression headerRe(
R"(^\[(user|assistant|reasoning|call|tool|error|info)(?::([^\]]*))?\](.*)$)",
QRegularExpression::MultilineOption);
auto it = headerRe.globalMatch(text);
QList<QPair<int, QRegularExpressionMatch>> headers;
while (it.hasNext()) {
auto match = it.next();
headers.append({match.capturedStart(), match});
}
if (headers.isEmpty()) {
ChatBlock b;
b.type = ChatBlock::Assistant;
b.content = text;
blocks.append(b);
return blocks;
}
for (int i = 0; i < headers.size(); ++i) {
const auto &[pos, match] = headers[i];
QString typeStr = match.captured(1);
QString name = match.captured(2);
QString inlineContent = match.captured(3);
int contentStart = match.capturedEnd();
int contentEnd = (i + 1 < headers.size()) ? headers[i + 1].first : text.size();
QString content;
if (typeStr == "info" && !inlineContent.trimmed().isEmpty()) {
content = inlineContent.trimmed();
} else {
content = text.mid(contentStart, contentEnd - contentStart);
if (content.startsWith('\n'))
content = content.mid(1);
while (content.endsWith("\n\n"))
content.chop(1);
}
ChatBlock b;
if (typeStr == "user") b.type = ChatBlock::User;
else if (typeStr == "assistant") b.type = ChatBlock::Assistant;
else if (typeStr == "reasoning") b.type = ChatBlock::Reasoning;
else if (typeStr == "call") b.type = ChatBlock::Call;
else if (typeStr == "tool") b.type = ChatBlock::Tool;
else if (typeStr == "error") b.type = ChatBlock::Error;
else if (typeStr == "info") b.type = ChatBlock::Info;
b.name = name;
b.content = content;
blocks.append(b);
}
return blocks;
}
QList<SubBlock> ChatRenderer::parseSubBlocks(const QString &content) const
{
QList<SubBlock> result;
if (content.isEmpty()) return result;
static const QRegularExpression fenceRe(
R"(^```(\w*)\s*\n((?:(?!^```).)*?)\n^```\s*$)",
QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption);
int lastEnd = 0;
auto it = fenceRe.globalMatch(content);
while (it.hasNext()) {
auto match = it.next();
if (match.capturedStart() > lastEnd) {
QString prose = content.mid(lastEnd, match.capturedStart() - lastEnd).trimmed();
if (!prose.isEmpty()) {
SubBlock sb;
sb.type = SubBlock::Prose;
sb.content = prose;
result.append(sb);
}
}
QString lang = match.captured(1);
QString code = match.captured(2);
SubBlock sb;
if (lang == "diff" || (lang.isEmpty() &&
(code.startsWith("--- ") || code.startsWith("diff --git")))) {
sb.type = SubBlock::Diff;
} else {
sb.type = SubBlock::Code;
}
sb.language = lang;
sb.content = code;
result.append(sb);
lastEnd = match.capturedEnd();
}
if (lastEnd < content.size()) {
QString prose = content.mid(lastEnd).trimmed();
if (!prose.isEmpty()) {
SubBlock sb;
sb.type = SubBlock::Prose;
sb.content = prose;
result.append(sb);
}
}
if (result.isEmpty()) {
SubBlock sb;
sb.type = SubBlock::Prose;
sb.content = content;
result.append(sb);
}
return result;
}
// --- HTML Rendering ---
QString ChatRenderer::highlightCode(const QString &code, const QString &language) const
{
auto def = m_repo->definitionForName(language);
if (!def.isValid())
def = m_repo->definitionForFileName("file." + language);
if (!def.isValid())
return QStringLiteral("<pre style=\"margin:4px 0;white-space:pre-wrap;\">") + escapeHtml(code) + QStringLiteral("</pre>");
auto theme = m_dark
? m_repo->defaultTheme(KSyntaxHighlighting::Repository::DarkTheme)
: m_repo->defaultTheme(KSyntaxHighlighting::Repository::LightTheme);
HtmlHighlighter builder;
builder.setDefinition(def);
builder.setTheme(theme);
KSyntaxHighlighting::State state;
const QStringList lines = code.split('\n');
for (int i = 0; i < lines.size(); ++i) {
state = builder.processLine(lines[i], state);
if (i < lines.size() - 1)
builder.html += '\n';
}
return QStringLiteral("<pre style=\"margin:4px 0;white-space:pre-wrap;\">") + builder.html + QStringLiteral("</pre>");
}
QString ChatRenderer::renderDiff(const QString &diff) const
{
QString html = QStringLiteral("<pre style=\"margin:4px 0;white-space:pre-wrap;\">");
const QStringList lines = diff.split('\n');
for (const QString &line : lines) {
if (line.startsWith('+')) {
html += QStringLiteral("<span style=\"color:#4caf50;\">") + escapeHtml(line) + QStringLiteral("</span>\n");
} else if (line.startsWith('-')) {
html += QStringLiteral("<span style=\"color:#f44336;\">") + escapeHtml(line) + QStringLiteral("</span>\n");
} else if (line.startsWith("@@")) {
html += QStringLiteral("<span style=\"color:#2196f3;\">") + escapeHtml(line) + QStringLiteral("</span>\n");
} else {
html += escapeHtml(line) + '\n';
}
}
html += QStringLiteral("</pre>");
return html;
}
QString ChatRenderer::renderProse(const QString &prose) const
{
// Simple markdown-ish rendering: bold, italic, inline code, links
QString html = escapeHtml(prose);
// Inline code: `code`
static const QRegularExpression inlineCode("`([^`]+)`");
html.replace(inlineCode, QStringLiteral("<code style=\"background:") +
(m_dark ? QStringLiteral("#333") : QStringLiteral("#f0f0f0")) +
QStringLiteral(";padding:1px 4px;border-radius:3px;\">\\1</code>"));
// Bold: **text** or __text__
static const QRegularExpression bold(R"(\*\*(.+?)\*\*)");
html.replace(bold, QStringLiteral("<b>\\1</b>"));
static const QRegularExpression bold2(R"(__(.+?)__)");
html.replace(bold2, QStringLiteral("<b>\\1</b>"));
// Italic: *text* or _text_
static const QRegularExpression italic(R"((?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*))");
html.replace(italic, QStringLiteral("<i>\\1</i>"));
// Bullet points: lines starting with - or *
static const QRegularExpression bullet(R"(^(\s*)[\-\*]\s)", QRegularExpression::MultilineOption);
html.replace(bullet, QStringLiteral("\\1• "));
// Newlines to <br>
html.replace('\n', QStringLiteral("<br>"));
return html;
}
QString ChatRenderer::subBlockToHtml(const SubBlock &sub) const
{
QString codeBg = m_dark ? QStringLiteral("#1e1e2e") : QStringLiteral("#f5f5f5");
QString codeBorder = m_dark ? QStringLiteral("#444") : QStringLiteral("#ddd");
switch (sub.type) {
case SubBlock::Code: {
QString langLabel;
if (!sub.language.isEmpty()) {
langLabel = QStringLiteral(
"<div style=\"font-size:small;color:gray;padding:2px 8px;\">") +
escapeHtml(sub.language) + QStringLiteral("</div>");
}
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:6px 0;\">"
"<tr><td style=\"background:") + codeBg +
QStringLiteral(";border:1px solid ") + codeBorder +
QStringLiteral(";padding:8px;\">") +
langLabel + highlightCode(sub.content, sub.language) +
QStringLiteral("</td></tr></table>");
}
case SubBlock::Diff: {
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:6px 0;\">"
"<tr><td style=\"background:") + codeBg +
QStringLiteral(";border:1px solid ") + codeBorder +
QStringLiteral(";padding:8px;\">"
"<div style=\"font-size:small;color:gray;padding:2px 8px;\">diff</div>") +
renderDiff(sub.content) +
QStringLiteral("</td></tr></table>");
}
case SubBlock::Prose:
return QStringLiteral("<div style=\"margin:4px 0;line-height:1.4;\">") +
renderProse(sub.content) + QStringLiteral("</div>");
}
return {};
}
QString ChatRenderer::blockToHtml(const ChatBlock &block) const
{
detectTheme();
QString userBg = m_dark ? QStringLiteral("#1a3a5c") : QStringLiteral("#e3f2fd");
QString assistantBg = m_dark ? QStringLiteral("#2a2a2a") : QStringLiteral("#fafafa");
QString errorBg = m_dark ? QStringLiteral("#3d1515") : QStringLiteral("#ffebee");
QString metaBg = m_dark ? QStringLiteral("#1a1a2e") : QStringLiteral("#f5f5f5");
QString metaColor = m_dark ? QStringLiteral("#888") : QStringLiteral("#666");
QString borderColor = m_dark ? QStringLiteral("#444") : QStringLiteral("#e0e0e0");
switch (block.type) {
case ChatBlock::User: {
QList<SubBlock> subs = parseSubBlocks(block.content);
QString inner;
for (const auto &sub : subs)
inner += subBlockToHtml(sub);
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0;\">"
"<tr><td width=\"15%\"></td>"
"<td style=\"background:") + userBg +
QStringLiteral(";padding:10px 14px;border:1px solid ") + borderColor +
QStringLiteral(";\">") + inner +
QStringLiteral("</td></tr>"
"<tr><td></td><td align=\"right\"><span style=\"font-size:small;color:gray;\">You</span></td></tr>"
"</table>");
}
case ChatBlock::Assistant: {
QList<SubBlock> subs = parseSubBlocks(block.content);
QString inner;
for (const auto &sub : subs)
inner += subBlockToHtml(sub);
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0;\">"
"<tr><td style=\"background:") + assistantBg +
QStringLiteral(";padding:10px 14px;border:1px solid ") + borderColor +
QStringLiteral(";\">") + inner +
QStringLiteral("</td><td width=\"5%\"></td></tr>"
"<tr><td><span style=\"font-size:small;color:gray;\">Ollie</span></td><td></td></tr>"
"</table>");
}
case ChatBlock::Reasoning: {
// Show abbreviated with dimmed styling
QString abbreviated = block.content.left(200);
if (block.content.size() > 200)
abbreviated += QStringLiteral("...");
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:4px 0;\">"
"<tr><td style=\"background:") + metaBg +
QStringLiteral(";padding:6px 10px;border-left:3px solid ") + metaColor +
QStringLiteral(";\">"
"<div style=\"font-size:small;color:") + metaColor +
QStringLiteral(";\">▸ reasoning</div>"
"<pre style=\"margin:4px 0;white-space:pre-wrap;color:") + metaColor +
QStringLiteral(";font-size:small;\">") + escapeHtml(abbreviated) +
QStringLiteral("</pre></td></tr></table>");
}
case ChatBlock::Call: {
QString label = QStringLiteral("⚡ call");
if (!block.name.isEmpty())
label += QStringLiteral(": ") + escapeHtml(block.name);
// Show args abbreviated
QString content = block.content.trimmed();
QString abbreviated = content.left(150);
if (content.size() > 150)
abbreviated += QStringLiteral("...");
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:4px 0;\">"
"<tr><td style=\"background:") + metaBg +
QStringLiteral(";padding:6px 10px;border-left:3px solid #ff9800;\">"
"<div style=\"font-size:small;color:#ff9800;\">") + label +
QStringLiteral("</div>"
"<pre style=\"margin:4px 0;white-space:pre-wrap;color:") + metaColor +
QStringLiteral(";font-size:small;\">") + escapeHtml(abbreviated) +
QStringLiteral("</pre></td></tr></table>");
}
case ChatBlock::Tool: {
QString label = QStringLiteral("◆ tool");
if (!block.name.isEmpty())
label += QStringLiteral(": ") + escapeHtml(block.name);
// Show result abbreviated
QString content = block.content.trimmed();
QString abbreviated = content.left(200);
if (content.size() > 200)
abbreviated += QStringLiteral("...");
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:4px 0;\">"
"<tr><td style=\"background:") + metaBg +
QStringLiteral(";padding:6px 10px;border-left:3px solid ") + metaColor +
QStringLiteral(";\">"
"<div style=\"font-size:small;color:") + metaColor +
QStringLiteral(";\">" ) + label +
QStringLiteral("</div>"
"<pre style=\"margin:4px 0;white-space:pre-wrap;color:") + metaColor +
QStringLiteral(";font-size:small;\">") + escapeHtml(abbreviated) +
QStringLiteral("</pre></td></tr></table>");
}
case ChatBlock::Error: {
return QStringLiteral(
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:6px 0;\">"
"<tr><td style=\"background:") + errorBg +
QStringLiteral(";padding:8px 12px;border-left:3px solid #e53935;\">"
"<pre style=\"margin:0;white-space:pre-wrap;color:#e53935;\">") +
escapeHtml(block.content) +
QStringLiteral("</pre></td></tr></table>");
}
case ChatBlock::Info: {
return QStringLiteral(
"<div style=\"margin:4px 0;font-style:italic;color:") + metaColor +
QStringLiteral(";font-size:small;\">") +
escapeHtml(block.content) + QStringLiteral("</div>");
}
}
return {};
}
// --- Full Render ---
void ChatRenderer::render(QTextEdit *view, const QString &rawText)
{
detectTheme();
QList<ChatBlock> blocks = parseBlocks(rawText);
QString html = QStringLiteral(
"<html><body style=\"font-family:sans-serif;font-size:medium;\">");
for (const auto &block : blocks)
html += blockToHtml(block);
html += QStringLiteral("</body></html>");
auto *sb = view->verticalScrollBar();
bool atBottom = (sb->value() >= sb->maximum() - 4);
view->setHtml(html);
if (atBottom)
sb->setValue(sb->maximum());
m_lastRawText = rawText;
m_lastBlockCount = blocks.size();
}
bool ChatRenderer::renderIncremental(QTextEdit *view, const QString &rawText)
{
// If text is just appended, try incremental
if (!rawText.startsWith(m_lastRawText) || m_lastRawText.isEmpty()) {
render(view, rawText);
return false;
}
// Full re-render (the HTML approach doesn't support true incremental well
// since setHtml replaces everything). For performance with typical chat
// sizes this is fine — QTextEdit handles it efficiently.
render(view, rawText);
return true;
}

80
kate/chat_renderer.h Normal file
View File

@ -0,0 +1,80 @@
#ifndef OLLIE_CHAT_RENDERER_H
#define OLLIE_CHAT_RENDERER_H
#include <QString>
#include <QList>
#include <QTextEdit>
#include <QRegularExpression>
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 };
Type type;
QString language;
QString content;
};
/**
* ChatRenderer parses ollie chat output into structured blocks
* and renders them as styled HTML in a QTextEdit.
*/
class ChatRenderer
{
public:
ChatRenderer();
~ChatRenderer();
/// 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
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);
private:
QString blockToHtml(const ChatBlock &block) const;
QString subBlockToHtml(const SubBlock &sub) 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;
QString m_lastRawText; // for incremental rendering
int m_lastBlockCount = 0;
// Colors (derived from palette)
mutable bool m_dark = false;
void detectTheme() const;
};
#endif // OLLIE_CHAT_RENDERER_H

View File

@ -1,5 +1,6 @@
#include "ollie_kate.h"
#include "ollie_ghost.h"
#include "chat_renderer.h"
#include <KPluginFactory>
#include <QKeyEvent>
@ -224,6 +225,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
: QObject(mainWindow)
, m_plugin(plugin)
, m_mainWindow(mainWindow)
, m_renderer(new ChatRenderer())
{
m_iface = new QDBusInterface(SERVICE, OBJ_PATH, IFACE,
QDBusConnection::sessionBus(), this);
@ -394,6 +396,7 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
OllieKateView::~OllieKateView()
{
delete m_renderer;
delete m_ghost;
delete m_toolView;
}
@ -405,18 +408,18 @@ void OllieKateView::onChatUpdated(const QString &session_id, qlonglong offset, c
if (session_id != m_sessionId) return;
if (offset == m_chatOffset) {
appendChat(new_text);
m_rawChat += new_text;
m_chatOffset += new_text.size();
m_renderer->render(m_chatView, m_rawChat);
} else {
// Offset mismatch — refetch
QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetChat");
msg << m_sessionId << (qlonglong)0;
QDBusMessage reply = QDBusConnection::sessionBus().call(msg);
if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() >= 2) {
m_chatView->clear();
m_chatView->setPlainText(reply.arguments()[0].toString());
m_rawChat = reply.arguments()[0].toString();
m_chatOffset = reply.arguments()[1].toLongLong();
m_chatView->verticalScrollBar()->setValue(m_chatView->verticalScrollBar()->maximum());
m_renderer->render(m_chatView, m_rawChat);
m_scrollBottomBtn->hide();
}
}
@ -447,7 +450,8 @@ void OllieKateView::onSessionKilled(const QString &session_id)
if (session_id == m_sessionId) {
m_sessionId.clear();
m_chatOffset = 0;
m_chatView->setPlainText("");
m_rawChat.clear();
m_chatView->clear();
m_sessionLabel->setText("No session");
m_cwdLabel->setText("");
refreshSessions();
@ -517,12 +521,13 @@ void OllieKateView::newSession()
QDBusReply<QString> reply = m_iface->call("CreateSession", cwd, backend, model, "default");
if (reply.isValid()) {
m_sessionId = reply.value();
m_rawChat.clear();
m_chatView->clear();
m_chatOffset = 0;
m_sessionLabel->setText("○ " + m_sessionId + " [idle]");
m_cwdLabel->setText(cwd);
} else {
m_chatView->setPlainText("Error creating session: " + reply.error().message());
m_chatView->setHtml("<p style='color:#e53935;'>Error creating session: " + reply.error().message().toHtmlEscaped() + "</p>");
}
}
@ -530,13 +535,13 @@ void OllieKateView::pickSession()
{
QString baseDir = projectBaseDir();
if (baseDir.isEmpty()) {
m_chatView->setPlainText("Cannot determine project directory.");
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>Cannot determine project directory.</p>"));
return;
}
QDBusReply<QStringList> reply = m_iface->call("ListSessions");
if (!reply.isValid() || reply.value().isEmpty()) {
m_chatView->setPlainText("No active sessions found.");
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No active sessions found.</p>"));
return;
}
@ -564,13 +569,13 @@ void OllieKateView::pickSession()
}
}
m_chatView->setPlainText("No session found with cwd: " + baseDir);
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No session found with cwd: ") + baseDir.toHtmlEscaped() + QStringLiteral("</p>"));
}
void OllieKateView::renameSession()
{
if (m_sessionId.isEmpty()) {
m_chatView->setPlainText("No active session to rename.");
m_chatView->setHtml(QStringLiteral("<p style='color:gray;font-style:italic;'>No active session to rename.</p>"));
return;
}
@ -596,7 +601,7 @@ void OllieKateView::renameSession()
m_sessionLabel->setText(dot + m_sessionId + " [idle]");
}
} else {
m_chatView->setPlainText("Failed to rename session.");
m_chatView->setHtml(QStringLiteral("<p style='color:#e53935;'>Failed to rename session.</p>"));
}
}
@ -623,6 +628,7 @@ QString OllieKateView::projectBaseDir()
void OllieKateView::switchToSession(const QString &sessionId)
{
m_sessionId = sessionId;
m_rawChat.clear();
m_chatView->clear();
m_chatOffset = 0;
@ -648,9 +654,9 @@ void OllieKateView::switchToSession(const QString &sessionId)
msg << m_sessionId << (qlonglong)0;
QDBusMessage chatReply = QDBusConnection::sessionBus().call(msg);
if (chatReply.type() == QDBusMessage::ReplyMessage && chatReply.arguments().size() >= 2) {
m_chatView->setPlainText(chatReply.arguments()[0].toString());
m_rawChat = chatReply.arguments()[0].toString();
m_chatOffset = chatReply.arguments()[1].toLongLong();
m_chatView->verticalScrollBar()->setValue(m_chatView->verticalScrollBar()->maximum());
m_renderer->render(m_chatView, m_rawChat);
m_scrollBottomBtn->hide();
}
}
@ -659,7 +665,7 @@ void OllieKateView::ensureSession()
{
if (!m_sessionId.isEmpty()) return;
if (!m_iface->isValid()) {
m_chatView->setPlainText("Error: ollie-kde daemon not running.\nStart with: ollie-kde or rc-service --user ollie-kde start");
m_chatView->setHtml(QStringLiteral("<p style='color:#e53935;'>Error: ollie-kde daemon not running.<br>Start with: ollie-kde or rc-service --user ollie-kde start</p>"));
return;
}
newSession();
@ -670,23 +676,6 @@ QString OllieKateView::activeSessionId()
return m_sessionId;
}
void OllieKateView::appendChat(const QString &text)
{
auto *sb = m_chatView->verticalScrollBar();
bool atBottom = (sb->value() >= sb->maximum() - 4);
QTextCursor cursor = m_chatView->textCursor();
cursor.movePosition(QTextCursor::End);
cursor.insertText(text);
if (atBottom) {
sb->setValue(sb->maximum());
m_scrollBottomBtn->hide();
} else {
m_scrollBottomBtn->show();
}
}
void OllieKateView::refreshSessions()
{
QDBusReply<QStringList> reply = m_iface->call("ListSessions");

View File

@ -18,6 +18,8 @@
#include <QAction>
#include <QEvent>
class ChatRenderer;
class QPushButton;
class OllieKateView;
class OllieGhostProvider;
@ -93,7 +95,6 @@ private slots:
private:
void ensureSession();
QString activeSessionId();
void appendChat(const QString &text);
OllieKatePlugin *m_plugin;
KTextEditor::MainWindow *m_mainWindow;
@ -112,6 +113,8 @@ private:
QString m_sessionId;
qlonglong m_chatOffset = 0;
QString m_rawChat; // accumulated raw chat text
ChatRenderer *m_renderer;
QPointer<OllieGhostProvider> m_ghost;
bool m_ghostPrefix = false; // true after Meta+O, waiting for second key