kate: simplify plugin — remove chat panel, keep context actions + ghost text
Strip the plugin down to its core value: - Context menu actions (explain, fix, refactor, document, test, review diff) - Ghost text completion (unchanged) - Git++ review diff integration - Auto agent-per-project matching (silent, no UI) - Status bar indicator showing current agent Removed: - Chat panel (side panel UI, streaming, renderer) - Inline annotations - Emoji processing - Config page (use env vars: OLLIE_COMPLETE_BACKEND, etc) - Apply code blocks - libkateprivate/diff viewer integration 3109 LOC → 1192 LOC. Use the KDE GUI app tiled alongside Kate for chat.
This commit is contained in:
parent
9a1704d728
commit
7aad74dafe
|
|
@ -21,22 +21,6 @@ include(KDECompilerSettings NO_POLICY_SCOPE)
|
|||
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||
|
||||
# --- Find libkateprivate for Kate's built-in diff viewer ---
|
||||
file(GLOB _KATEPRIVATE_CANDIDATES
|
||||
"/usr/lib64/libkateprivate.so*"
|
||||
"/usr/lib/libkateprivate.so*"
|
||||
"/usr/lib/x86_64-linux-gnu/libkateprivate.so*"
|
||||
"/usr/lib/aarch64-linux-gnu/libkateprivate.so*")
|
||||
if(_KATEPRIVATE_CANDIDATES)
|
||||
list(GET _KATEPRIVATE_CANDIDATES 0 KATEPRIVATE_LIB)
|
||||
message(STATUS "Found libkateprivate: ${KATEPRIVATE_LIB}")
|
||||
set(HAVE_KATE_DIFF TRUE)
|
||||
else()
|
||||
set(KATEPRIVATE_LIB "")
|
||||
set(HAVE_KATE_DIFF FALSE)
|
||||
message(STATUS "libkateprivate not found -- diffs will render inline")
|
||||
endif()
|
||||
|
||||
if(OLLIE_KF5)
|
||||
# --- Qt5 / KF5 ---
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Concurrent Widgets Quick Qml QuickControls2 Network)
|
||||
|
|
@ -64,7 +48,6 @@ if(OLLIE_KF5)
|
|||
kcoreaddons_add_plugin(ollie_kate SOURCES
|
||||
kate/ollie_kate.cpp
|
||||
kate/ollie_ghost.cpp
|
||||
kate/chat_renderer.cpp
|
||||
gui/lib9pclient.cpp
|
||||
INSTALL_NAMESPACE "ktexteditor"
|
||||
)
|
||||
|
|
@ -79,11 +62,6 @@ if(OLLIE_KF5)
|
|||
Qt5::Widgets
|
||||
$ENV{HOME}/.local/lib/libollie9p.so
|
||||
)
|
||||
if(HAVE_KATE_DIFF)
|
||||
target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
|
||||
target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
|
||||
target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
|
||||
endif()
|
||||
|
||||
# --- GUI App (9P-based) ---
|
||||
# Strip Qt6-only grouped palette syntax (palette.active.*, palette.inactive.*,
|
||||
|
|
@ -146,7 +124,6 @@ else()
|
|||
kcoreaddons_add_plugin(ollie_kate SOURCES
|
||||
kate/ollie_kate.cpp
|
||||
kate/ollie_ghost.cpp
|
||||
kate/chat_renderer.cpp
|
||||
gui/lib9pclient.cpp
|
||||
INSTALL_NAMESPACE "kf6/ktexteditor"
|
||||
)
|
||||
|
|
@ -161,11 +138,6 @@ else()
|
|||
Qt6::Widgets
|
||||
$ENV{HOME}/.local/lib/libollie9p.so
|
||||
)
|
||||
if(HAVE_KATE_DIFF)
|
||||
target_compile_definitions(ollie_kate PRIVATE HAVE_KATE_DIFF)
|
||||
target_include_directories(ollie_kate PRIVATE ${CMAKE_SOURCE_DIR}/kate)
|
||||
target_link_libraries(ollie_kate ${KATEPRIVATE_LIB})
|
||||
endif()
|
||||
|
||||
# --- GUI App (9P-based, no D-Bus) ---
|
||||
find_package(KF6SyntaxHighlighting REQUIRED)
|
||||
|
|
|
|||
|
|
@ -1,714 +0,0 @@
|
|||
#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);
|
||||
|
||||
static const QRegularExpression endRe(
|
||||
R"(^\[\[\[end\]\]\]\s*$)",
|
||||
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();
|
||||
|
||||
// Find the [[[end]]] marker after this header (before next header)
|
||||
int searchEnd = (i + 1 < headers.size()) ? headers[i + 1].first : text.size();
|
||||
QString segment = text.mid(contentStart, searchEnd - contentStart);
|
||||
auto endMatch = endRe.match(segment);
|
||||
|
||||
int contentEnd;
|
||||
if (endMatch.hasMatch()) {
|
||||
contentEnd = contentStart + endMatch.capturedStart();
|
||||
} else {
|
||||
// No [[[end]]] found — treat rest until next header as content (streaming)
|
||||
contentEnd = searchEnd;
|
||||
}
|
||||
|
||||
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 interruptRe(
|
||||
R"(<system-user-interruption>\n?(.*?)\n?</system-user-interruption>)",
|
||||
QRegularExpression::DotMatchesEverythingOption);
|
||||
|
||||
static const QRegularExpression fenceRe(
|
||||
R"(^```(\w*)[ \t]*([^\n]*)\n((?:(?!^```).)*?)\n^```\s*$)",
|
||||
QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption);
|
||||
|
||||
// Find all interrupt tags
|
||||
int lastEnd = 0;
|
||||
auto intIt = interruptRe.globalMatch(content);
|
||||
QList<QPair<int, QRegularExpressionMatch>> interrupts;
|
||||
while (intIt.hasNext()) {
|
||||
auto match = intIt.next();
|
||||
interrupts.append({match.capturedStart(), match});
|
||||
}
|
||||
|
||||
// Helper: parse a segment for code fences and prose
|
||||
auto parseSegment = [&](const QString &segment) {
|
||||
if (segment.trimmed().isEmpty()) return;
|
||||
|
||||
int segLastEnd = 0;
|
||||
auto it = fenceRe.globalMatch(segment);
|
||||
while (it.hasNext()) {
|
||||
auto match = it.next();
|
||||
if (match.capturedStart() > segLastEnd) {
|
||||
QString prose = segment.mid(segLastEnd, match.capturedStart() - segLastEnd).trimmed();
|
||||
if (!prose.isEmpty()) {
|
||||
SubBlock sb;
|
||||
sb.type = SubBlock::Prose;
|
||||
sb.content = prose;
|
||||
result.append(sb);
|
||||
}
|
||||
}
|
||||
|
||||
QString lang = match.captured(1);
|
||||
QString meta = match.captured(2).trimmed();
|
||||
QString code = match.captured(3);
|
||||
|
||||
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.fenceMeta = meta;
|
||||
sb.content = code;
|
||||
result.append(sb);
|
||||
|
||||
segLastEnd = match.capturedEnd();
|
||||
}
|
||||
|
||||
if (segLastEnd < segment.size()) {
|
||||
QString prose = segment.mid(segLastEnd).trimmed();
|
||||
if (!prose.isEmpty()) {
|
||||
SubBlock sb;
|
||||
sb.type = SubBlock::Prose;
|
||||
sb.content = prose;
|
||||
result.append(sb);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (interrupts.isEmpty()) {
|
||||
parseSegment(content);
|
||||
} else {
|
||||
for (int i = 0; i < interrupts.size(); ++i) {
|
||||
const auto &[pos, match] = interrupts[i];
|
||||
|
||||
if (pos > lastEnd) {
|
||||
parseSegment(content.mid(lastEnd, pos - lastEnd));
|
||||
}
|
||||
|
||||
SubBlock sb;
|
||||
sb.type = SubBlock::Interrupt;
|
||||
sb.content = match.captured(1).trimmed();
|
||||
result.append(sb);
|
||||
|
||||
lastEnd = match.capturedEnd();
|
||||
}
|
||||
|
||||
if (lastEnd < content.size()) {
|
||||
parseSegment(content.mid(lastEnd));
|
||||
}
|
||||
}
|
||||
|
||||
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, bool preserveWhitespace) 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: {
|
||||
// Store diff and render a clickable link to open in Kate's diff viewer
|
||||
if (m_diffStorage) {
|
||||
int idx = m_diffStorage->size();
|
||||
m_diffStorage->append(sub.content);
|
||||
|
||||
// Count lines for display
|
||||
int adds = 0, dels = 0;
|
||||
const QStringList lines = sub.content.split('\n');
|
||||
for (const QString &line : lines) {
|
||||
if (line.startsWith('+') && !line.startsWith("+++")) adds++;
|
||||
else if (line.startsWith('-') && !line.startsWith("---")) dels++;
|
||||
}
|
||||
QString stats;
|
||||
if (adds > 0) stats += QStringLiteral("<span style=\"color:#4caf50;\">+%1</span> ").arg(adds);
|
||||
if (dels > 0) stats += QStringLiteral("<span style=\"color:#f44336;\">-%1</span>").arg(dels);
|
||||
|
||||
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 12px;\">"
|
||||
"<span style=\"font-size:small;color:gray;\">▶ diff</span> ") +
|
||||
stats +
|
||||
QStringLiteral("<br><a href=\"ollie-diff:///") + QString::number(idx) +
|
||||
QStringLiteral("\" style=\"color:#2196f3;text-decoration:underline;\">"
|
||||
"↗ Open in Diff Viewer</a>"
|
||||
"</td></tr></table>");
|
||||
}
|
||||
|
||||
// Fallback: render inline
|
||||
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:
|
||||
if (preserveWhitespace) {
|
||||
return QStringLiteral("<pre style=\"margin:4px 0;white-space:pre-wrap;font-family:monospace;font-size:small;\">") +
|
||||
escapeHtml(sub.content) + QStringLiteral("</pre>");
|
||||
}
|
||||
return QStringLiteral("<div style=\"margin:4px 0;line-height:1.4;\">") +
|
||||
renderProse(sub.content) + QStringLiteral("</div>");
|
||||
case SubBlock::Interrupt: {
|
||||
QString interruptBg = m_dark ? QStringLiteral("#3d2e00") : QStringLiteral("#fff8e1");
|
||||
QString interruptBorder = QStringLiteral("#ffc107");
|
||||
return QStringLiteral(
|
||||
"<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin:8px 0;\">"
|
||||
"<tr><td style=\"background:") + interruptBg +
|
||||
QStringLiteral(";padding:8px 12px;border-left:3px solid ") + interruptBorder +
|
||||
QStringLiteral(";\">"
|
||||
"<div style=\"font-size:small;color:") + interruptBorder +
|
||||
QStringLiteral(";font-weight:bold;\">⚠ user interrupt</div>"
|
||||
"<div style=\"margin:4px 0;line-height:1.4;\">") +
|
||||
renderProse(sub.content) +
|
||||
QStringLiteral("</div></td></tr></table>");
|
||||
}
|
||||
}
|
||||
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;
|
||||
QString lastProse; // track preceding prose for file path extraction
|
||||
QString lastFilePath; // carry forward across code blocks
|
||||
for (const auto &sub : subs) {
|
||||
if (sub.type == SubBlock::Prose) {
|
||||
lastProse = sub.content;
|
||||
}
|
||||
inner += subBlockToHtml(sub);
|
||||
|
||||
// Add "Apply" button for code blocks in assistant messages
|
||||
if (sub.type == SubBlock::Code && m_applyStorage && !sub.content.trimmed().isEmpty()) {
|
||||
// Parse fence metadata for /path:startLine-endLine
|
||||
static const QRegularExpression fenceMetaRe(
|
||||
R"((/[^\s:]+)(?::(\d+)(?:-(\d+))?)?)");
|
||||
QString filePath;
|
||||
int startLine = -1, endLine = -1;
|
||||
|
||||
auto metaMatch = fenceMetaRe.match(sub.fenceMeta);
|
||||
if (metaMatch.hasMatch()) {
|
||||
filePath = metaMatch.captured(1);
|
||||
lastFilePath = filePath;
|
||||
if (!metaMatch.captured(2).isEmpty())
|
||||
startLine = metaMatch.captured(2).toInt();
|
||||
if (!metaMatch.captured(3).isEmpty())
|
||||
endLine = metaMatch.captured(3).toInt();
|
||||
else if (startLine > 0)
|
||||
endLine = startLine; // single line
|
||||
} else {
|
||||
// Fall back to extracting path from preceding prose
|
||||
static const QRegularExpression filePathRe(
|
||||
R"((?:`|\b)(/[^\s:`]+\.\w+)(?:`|\b))");
|
||||
auto proseMatch = filePathRe.match(lastProse);
|
||||
if (proseMatch.hasMatch()) {
|
||||
filePath = proseMatch.captured(1);
|
||||
lastFilePath = filePath;
|
||||
} else {
|
||||
filePath = lastFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
int idx = m_applyStorage->size();
|
||||
ApplyBlock ab;
|
||||
ab.filePath = filePath;
|
||||
ab.code = sub.content;
|
||||
ab.startLine = startLine;
|
||||
ab.endLine = endLine;
|
||||
m_applyStorage->append(ab);
|
||||
|
||||
QString rangeStr;
|
||||
if (startLine > 0 && endLine > 0) {
|
||||
rangeStr = QStringLiteral(":%1-%2").arg(startLine).arg(endLine);
|
||||
}
|
||||
QString applyLabel = filePath.isEmpty()
|
||||
? QStringLiteral("✎ Apply")
|
||||
: QStringLiteral("✎ Apply to ") + escapeHtml(filePath.section('/', -1)) + rangeStr;
|
||||
inner += QStringLiteral(
|
||||
"<div style=\"margin:-4px 0 6px 0;\">"
|
||||
"<a href=\"ollie-apply:///") + QString::number(idx) +
|
||||
QStringLiteral("\" style=\"color:#4caf50;text-decoration:none;"
|
||||
"font-size:small;padding:2px 8px;"
|
||||
"border:1px solid #4caf50;border-radius:3px;\">") +
|
||||
applyLabel + QStringLiteral("</a></div>");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Render full call args with sub-block parsing (preserve whitespace)
|
||||
QList<SubBlock> subs = parseSubBlocks(block.content);
|
||||
QString inner;
|
||||
for (const auto &sub : subs)
|
||||
inner += subBlockToHtml(sub, true);
|
||||
|
||||
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>") + inner +
|
||||
QStringLiteral("</td></tr></table>");
|
||||
}
|
||||
case ChatBlock::Tool: {
|
||||
QString label = QStringLiteral("◆ tool");
|
||||
if (!block.name.isEmpty())
|
||||
label += QStringLiteral(": ") + escapeHtml(block.name);
|
||||
|
||||
// Render full tool output with sub-block parsing (preserve whitespace)
|
||||
QList<SubBlock> subs = parseSubBlocks(block.content);
|
||||
QString inner;
|
||||
for (const auto &sub : subs)
|
||||
inner += subBlockToHtml(sub, true);
|
||||
|
||||
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>") + inner +
|
||||
QStringLiteral("</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();
|
||||
|
||||
// Clear storage before re-render (blocks get re-indexed)
|
||||
if (m_diffStorage)
|
||||
m_diffStorage->clear();
|
||||
if (m_applyStorage)
|
||||
m_applyStorage->clear();
|
||||
|
||||
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;\">");
|
||||
|
||||
// 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();
|
||||
bool atBottom = (sb->maximum() <= 0) || (sb->value() >= sb->maximum() - 4);
|
||||
int savedPos = sb->value();
|
||||
|
||||
// Block signals to prevent intermediate scroll events during setHtml
|
||||
sb->blockSignals(true);
|
||||
view->setHtml(html);
|
||||
|
||||
if (atBottom) {
|
||||
// Move cursor to end and ensure visible — this forces layout and
|
||||
// scrolls reliably, unlike sb->setValue(sb->maximum()) which can
|
||||
// read a stale maximum before relayout completes.
|
||||
QTextCursor cursor = view->textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
view->setTextCursor(cursor);
|
||||
view->ensureCursorVisible();
|
||||
} else {
|
||||
// Restore exact scroll position — clamp to new maximum
|
||||
sb->setValue(qMin(savedPos, sb->maximum()));
|
||||
}
|
||||
sb->blockSignals(false);
|
||||
|
||||
m_lastRawText = rawText;
|
||||
m_lastBlockCount = m_allBlocks.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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
#ifndef OLLIE_CHAT_RENDERER_H
|
||||
#define OLLIE_CHAT_RENDERER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include <QStringList>
|
||||
#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, 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<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 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<ApplyBlock> *m_applyStorage = nullptr;
|
||||
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;
|
||||
};
|
||||
|
||||
#endif // OLLIE_CHAT_RENDERER_H
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
Minimal copy of Kate's DiffParams for external plugin use.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <functional>
|
||||
|
||||
struct DiffParams {
|
||||
enum Flag {
|
||||
ShowStage = 1,
|
||||
ShowUnstage = 2,
|
||||
ShowDiscard = 4,
|
||||
ShowFileName = 8,
|
||||
ShowCommitInfo = 16,
|
||||
ReloadOnShow = 32,
|
||||
ShowFullContext = 64,
|
||||
ShowEditLeftSide = 128,
|
||||
ShowEditRightSide = 256
|
||||
};
|
||||
Q_DECLARE_FLAGS(Flags, Flag)
|
||||
|
||||
QString tabTitle;
|
||||
QString srcFile;
|
||||
QString destFile;
|
||||
QString workingDir;
|
||||
QStringList arguments;
|
||||
Flags flags;
|
||||
|
||||
void clear()
|
||||
{
|
||||
tabTitle = srcFile = destFile = workingDir = QString();
|
||||
arguments.clear();
|
||||
flags = {};
|
||||
updateStatusCallback = nullptr;
|
||||
}
|
||||
|
||||
std::function<void()> updateStatusCallback;
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
Minimal declaration of Utils::showDiff for external plugin use.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "diffparams.h"
|
||||
#include <QByteArray>
|
||||
|
||||
namespace KTextEditor { class MainWindow; }
|
||||
|
||||
namespace Utils {
|
||||
void showDiff(const QByteArray &diff, const DiffParams ¶ms, KTextEditor::MainWindow *mainWindow);
|
||||
}
|
||||
1332
kate/ollie_kate.cpp
1332
kate/ollie_kate.cpp
File diff suppressed because it is too large
Load Diff
|
|
@ -2,28 +2,19 @@
|
|||
#define OLLIE_KATE_PLUGIN_H
|
||||
|
||||
#include <KTextEditor/Plugin>
|
||||
#include <KTextEditor/ConfigPage>
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
#if QT_VERSION_MAJOR >= 6
|
||||
#include <KPluginMetaData>
|
||||
#endif
|
||||
|
||||
#include <QProcess>
|
||||
#include <QPointer>
|
||||
#include <QTextBrowser>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QLabel>
|
||||
#include <QAction>
|
||||
#include <QEvent>
|
||||
#include <QStandardPaths>
|
||||
#include <KTextEditor/MovingRange>
|
||||
#include <KTextEditor/Attribute>
|
||||
|
||||
#include "chat_renderer.h"
|
||||
#include "lib9pclient.h"
|
||||
|
||||
class QPushButton;
|
||||
class OllieKateView;
|
||||
class OllieGhostProvider;
|
||||
|
||||
|
|
@ -40,37 +31,18 @@ public:
|
|||
~OllieKatePlugin() override = default;
|
||||
|
||||
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
|
||||
int configPages() const override;
|
||||
KTextEditor::ConfigPage *configPage(int number, QWidget *parent) override;
|
||||
|
||||
// Completion config accessors
|
||||
QString completionBackend() const;
|
||||
QString completionModel() const;
|
||||
void setCompletionBackend(const QString &backend);
|
||||
void setCompletionModel(const QString &model);
|
||||
bool completionEnabled() const;
|
||||
void setCompletionEnabled(bool enabled);
|
||||
|
||||
// Default session backend/model (overrides OLLIE_BACKEND / OLLIE_MODEL)
|
||||
QString defaultBackend() const;
|
||||
QString defaultModel() const;
|
||||
void setDefaultBackend(const QString &backend);
|
||||
void setDefaultModel(const QString &model);
|
||||
|
||||
// Diff view mode: "unified", "side-by-side", "raw"
|
||||
QString diffMode() const;
|
||||
void setDiffMode(const QString &mode);
|
||||
QString completionBackend() const { return m_completionBackend; }
|
||||
QString completionModel() const { return m_completionModel; }
|
||||
bool completionEnabled() const { return m_completionEnabled; }
|
||||
|
||||
private:
|
||||
void loadConfig();
|
||||
void saveConfig();
|
||||
|
||||
QString m_completionBackend;
|
||||
QString m_completionModel;
|
||||
bool m_completionEnabled = true;
|
||||
QString m_defaultBackend;
|
||||
QString m_defaultModel;
|
||||
QString m_diffMode;
|
||||
};
|
||||
|
||||
class OllieKateView : public QObject
|
||||
|
|
@ -87,83 +59,40 @@ protected:
|
|||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void onChatUpdated(const QString &session_id, qlonglong offset, const QString &new_text);
|
||||
void onStateChanged(const QString &session_id, const QString &new_state);
|
||||
void onSessionCreated(const QString &session_id);
|
||||
void onSessionKilled(const QString &session_id);
|
||||
void onProjectChanged();
|
||||
|
||||
void submitPrompt();
|
||||
private:
|
||||
// Actions
|
||||
void askAboutFile();
|
||||
void explainSelection();
|
||||
void newAgent();
|
||||
void pickAgent();
|
||||
void refreshSessions();
|
||||
void submitSelectionPrompt(const QString &action);
|
||||
void reviewDiff();
|
||||
|
||||
private:
|
||||
// Agent management
|
||||
void ensureAgent();
|
||||
bool selectAgentForProject();
|
||||
void switchToAgent(const QString &agentId);
|
||||
bool selectAgentForProject(); // Auto-select agent matching project basedir
|
||||
QString activeSessionId();
|
||||
QString projectBaseDir();
|
||||
QString agentPath() const;
|
||||
void updateStatusLabel();
|
||||
|
||||
// Context menu
|
||||
void setupContextMenu(KTextEditor::View *view);
|
||||
void setupGitPlusPlusDiffAction();
|
||||
|
||||
// Ghost text
|
||||
void setupGhostProvider();
|
||||
|
||||
OllieKatePlugin *m_plugin;
|
||||
KTextEditor::MainWindow *m_mainWindow;
|
||||
QWidget *m_toolView;
|
||||
QLabel *m_sessionLabel;
|
||||
QLabel *m_cwdLabel;
|
||||
QLabel *m_completionStatus;
|
||||
QTextBrowser *m_chatView;
|
||||
QPlainTextEdit *m_promptInput;
|
||||
QAction *m_askFileAction;
|
||||
QAction *m_explainAction;
|
||||
QAction *m_newSessionAction;
|
||||
QPushButton *m_stopBtn;
|
||||
QPushButton *m_scrollBottomBtn;
|
||||
|
||||
// 9P client (direct connection, no shell-out)
|
||||
Lib9pClient *m_9p;
|
||||
QString agentPath() const; // Returns "session/kate/agent/{m_agentId}"
|
||||
|
||||
// Streaming processes
|
||||
QProcess *m_chatProc = nullptr;
|
||||
QProcess *m_stateProc = nullptr;
|
||||
void startChatStream();
|
||||
void startStateStream();
|
||||
void stopStreams();
|
||||
|
||||
QString m_sessionId;
|
||||
QString m_agentId; // Agent within the kate session
|
||||
qlonglong m_chatOffset = 0;
|
||||
QString m_rawChat; // accumulated raw chat text
|
||||
ChatRenderer *m_renderer;
|
||||
QStringList m_diffBlocks; // stored diff content for viewer
|
||||
|
||||
void showDiffBlock(int index);
|
||||
void onAnchorClicked(const QUrl &url);
|
||||
QString m_agentId;
|
||||
|
||||
QLabel *m_statusLabel = nullptr;
|
||||
QPointer<OllieGhostProvider> m_ghost;
|
||||
bool m_ghostPrefix = false; // true after Meta+O, waiting for second key
|
||||
|
||||
void processEmojis();
|
||||
|
||||
QString projectBaseDir();
|
||||
void switchToSession(const QString &sessionId);
|
||||
void setupGhostProvider();
|
||||
|
||||
// --- Editor Integration ---
|
||||
void setupContextMenu(KTextEditor::View *view);
|
||||
void setupGitPlusPlusDiffAction();
|
||||
void submitSelectionPrompt(const QString &action);
|
||||
void reviewDiff();
|
||||
void applyCodeBlock(int index);
|
||||
void clearAnnotations();
|
||||
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;
|
||||
QPointer<KTextEditor::Document> m_annotationDoc;
|
||||
bool m_ghostPrefix = false;
|
||||
};
|
||||
|
||||
#endif // OLLIE_KATE_PLUGIN_H
|
||||
|
|
|
|||
Reference in New Issue