kate: integrate with project plugin git widget and diff viewer

- setupProjectGitWidget(): Hook into kateprojectplugin's GitWidget
  via gitWidget() method, install event filter on tree view
- addOllieActionsToGitContextMenu(): Add 'Review Staged Changes' and
  'Ask About Selection' to the git widget's context menu
- reviewStagedChanges(): Get staged (or unstaged) diff via git process
- askAboutGitSelection(): Ask Ollie about selected files in git view
- onWidgetAdded(): Detect DiffWidget additions by shouldClose() method
- onDiffWidgetContextMenu(): Add 'Review This Diff' to diff widget
- reviewDiffFromWidget(): Extract diff from widget and send to Ollie
This commit is contained in:
Levi Neely 2026-10-08 10:29:12 +02:00
parent ab4f0ef785
commit a546b8721a
2 changed files with 197 additions and 2 deletions

View File

@ -9,13 +9,17 @@
#include <QApplication>
#include <QCheckBox>
#include <QContextMenuEvent>
#include <QDir>
#include <QFileInfo>
#include <QHash>
#include <QKeyEvent>
#include <QMainWindow>
#include <QMenu>
#include <QPlainTextEdit>
#include <QProcess>
#include <QStatusBar>
#include <QTreeView>
#include <QVBoxLayout>
K_PLUGIN_CLASS_WITH_JSON(OllieKatePlugin, "metadata.json")
@ -167,8 +171,15 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m
if (auto *view = mainWindow->activeView())
setupContextMenu(view);
// Git++ "Review Diff" action
// Git++ "Review Diff" action (third-party plugin)
setupGitPlusPlusDiffAction();
// Kate project plugin's built-in Git widget integration
setupProjectGitWidget();
// Hook into widget additions for diff widget integration
connect(mainWindow, &KTextEditor::MainWindow::widgetAdded,
this, &OllieKateView::onWidgetAdded);
}
OllieKateView::~OllieKateView()
@ -452,6 +463,169 @@ void OllieKateView::setupGitPlusPlusDiffAction()
Q_ARG(QAction *, action));
}
void OllieKateView::setupProjectGitWidget()
{
QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
if (!projectView) return;
// Get the GitWidget via the gitWidget() method
QWidget *gitWidget = nullptr;
QMetaObject::invokeMethod(projectView, "gitWidget",
Qt::DirectConnection,
Q_RETURN_ARG(QWidget *, gitWidget));
if (!gitWidget) return;
// Find the tree view inside the git widget for context menu interception
auto *treeView = gitWidget->findChild<QTreeView *>();
if (treeView && treeView->viewport()) {
treeView->viewport()->installEventFilter(this);
m_gitTreeView = treeView;
}
}
void OllieKateView::onWidgetAdded(QWidget *widget)
{
// Check if this is a DiffWidget by looking for characteristic properties
// DiffWidget has a shouldClose() Q_INVOKABLE method
if (widget && widget->metaObject()->indexOfMethod("shouldClose()") >= 0) {
// This is likely a DiffWidget — install context menu handling
widget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(widget, &QWidget::customContextMenuRequested,
this, [this, widget](const QPoint &pos) {
onDiffWidgetContextMenu(widget, pos);
});
}
}
void OllieKateView::onDiffWidgetContextMenu(QWidget *diffWidget, const QPoint &pos)
{
QMenu menu(diffWidget);
auto *reviewAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-preview")),
QStringLiteral("Ollie: Review This Diff"));
connect(reviewAction, &QAction::triggered, this, [this, diffWidget]() {
reviewDiffFromWidget(diffWidget);
});
menu.exec(diffWidget->mapToGlobal(pos));
}
void OllieKateView::reviewDiffFromWidget(QWidget *diffWidget)
{
// Try to get the raw diff from the widget's m_rawDiff member
// DiffWidget stores the raw diff as a QByteArray
QByteArray diff;
// Try reading the rawDiff property if exposed, otherwise get text from editors
QVariant rawDiffVar = diffWidget->property("rawDiff");
if (rawDiffVar.isValid()) {
diff = rawDiffVar.toByteArray();
}
// If no rawDiff property, try to find the diff editors and get their text
if (diff.isEmpty()) {
auto editors = diffWidget->findChildren<QPlainTextEdit *>();
for (auto *editor : editors) {
QString text = editor->toPlainText();
if (text.contains(QLatin1String("diff --git")) ||
text.contains(QLatin1String("@@")) ||
text.contains(QLatin1String("--- a/"))) {
diff = text.toUtf8();
break;
}
}
}
if (diff.isEmpty()) return;
ensureAgent();
if (m_agentId.isEmpty()) return;
QString prompt = QStringLiteral("Read-only task. Review the following diff. Do not edit files. Identify correctness bugs, regressions, security issues, missing tests, and maintainability concerns. For each finding, explain the impact and cite the relevant hunk. If there are no findings, say so explicitly.\n\n```diff\n")
+ QString::fromUtf8(diff) + QStringLiteral("\n```");
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
}
void OllieKateView::addOllieActionsToGitContextMenu(const QPoint &)
{
// Find the active popup menu (Kate's git widget context menu)
QMenu *menu = qobject_cast<QMenu *>(QApplication::activePopupWidget());
if (!menu) return;
// Add separator and Ollie actions
menu->addSeparator();
auto *reviewAction = menu->addAction(QIcon::fromTheme(QStringLiteral("document-preview")),
QStringLiteral("Ollie: Review Staged Changes"));
connect(reviewAction, &QAction::triggered, this, &OllieKateView::reviewStagedChanges);
auto *askAction = menu->addAction(QIcon::fromTheme(QStringLiteral("help-about")),
QStringLiteral("Ollie: Ask About Selection"));
connect(askAction, &QAction::triggered, this, &OllieKateView::askAboutGitSelection);
}
void OllieKateView::reviewStagedChanges()
{
QString baseDir = projectBaseDir();
if (baseDir.isEmpty()) return;
// Get staged diff via git
QProcess git;
git.setWorkingDirectory(baseDir);
git.start(QStringLiteral("git"), {QStringLiteral("diff"), QStringLiteral("--cached")});
if (!git.waitForFinished(5000)) return;
QByteArray diff = git.readAllStandardOutput();
if (diff.trimmed().isEmpty()) {
// No staged changes, try unstaged
git.start(QStringLiteral("git"), {QStringLiteral("diff")});
if (!git.waitForFinished(5000)) return;
diff = git.readAllStandardOutput();
}
if (diff.trimmed().isEmpty()) return;
ensureAgent();
if (m_agentId.isEmpty()) return;
QString prompt = QStringLiteral("Read-only task. Review the following diff. Do not edit files. Identify correctness bugs, regressions, security issues, missing tests, and maintainability concerns. For each finding, explain the impact and cite the relevant hunk. If there are no findings, say so explicitly.\n\n```diff\n")
+ QString::fromUtf8(diff) + QStringLiteral("\n```");
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
}
void OllieKateView::askAboutGitSelection()
{
if (!m_gitTreeView) return;
// Get selected files from git tree view
QStringList files;
auto *selModel = m_gitTreeView->selectionModel();
if (selModel) {
const auto indexes = selModel->selectedIndexes();
for (const auto &idx : indexes) {
// Try to get file name from the model
QString fileName = idx.data(Qt::DisplayRole).toString();
if (!fileName.isEmpty() && !files.contains(fileName)) {
files.append(fileName);
}
}
}
if (files.isEmpty()) return;
QString baseDir = projectBaseDir();
if (baseDir.isEmpty()) return;
ensureAgent();
if (m_agentId.isEmpty()) return;
QString fileList = files.join(QStringLiteral(", "));
QString prompt = QStringLiteral("Read-only task. Inspect these files and describe their current git status and any changes: ") + fileList +
QStringLiteral("\n\nBase directory: ") + baseDir +
QStringLiteral("\n\nExplain what changes are pending for these files, whether they are staged or unstaged, and any notable patterns in the modifications.");
m_9p->write(agentPath() + "/prompt", prompt.toUtf8());
}
// --- Actions ---
void OllieKateView::askAboutFile()
@ -604,10 +778,20 @@ void OllieKateView::setupGhostProvider()
}
}
// --- Event Filter (ghost text key handling) ---
// --- Event Filter (ghost text key handling + git widget context menu) ---
bool OllieKateView::eventFilter(QObject *obj, QEvent *event)
{
// Handle context menu for git widget's tree view
if (event->type() == QEvent::ContextMenu && m_gitTreeView && obj == m_gitTreeView->viewport()) {
auto *contextEvent = static_cast<QContextMenuEvent *>(event);
// Don't block the event — let the original handler run first
// We'll add our items via a delayed single-shot
QTimer::singleShot(0, this, [this, pos = contextEvent->globalPos()]() {
addOllieActionsToGitContextMenu(pos);
});
}
if (event->type() == QEvent::KeyPress && m_ghost) {
auto *keyEvent = static_cast<QKeyEvent *>(event);

View File

@ -12,6 +12,7 @@
#include <QAction>
#include <QEvent>
#include <QTimer>
#include <QTreeView>
#include "lib9pclient.h"
@ -61,6 +62,7 @@ protected:
private slots:
void onProjectChanged();
void heartbeat();
void onWidgetAdded(QWidget *widget);
private:
// Actions
@ -70,6 +72,9 @@ private:
void sendVerbatim();
void reviewDiff();
void startSession();
void reviewStagedChanges();
void askAboutGitSelection();
void reviewDiffFromWidget(QWidget *diffWidget);
// Agent naming
QString agentNameFromCwd(const QString &cwd);
@ -85,6 +90,9 @@ private:
// Context menu
void setupContextMenu(KTextEditor::View *view);
void setupGitPlusPlusDiffAction();
void setupProjectGitWidget();
void onDiffWidgetContextMenu(QWidget *diffWidget, const QPoint &pos);
void addOllieActionsToGitContextMenu(const QPoint &globalPos);
// Ghost text
void setupGhostProvider();
@ -101,6 +109,9 @@ private:
QLabel *m_statusLabel = nullptr;
QPointer<OllieGhostProvider> m_ghost;
bool m_ghostPrefix = false;
// Git widget integration
QPointer<QTreeView> m_gitTreeView;
};
#endif // OLLIE_KATE_PLUGIN_H