From a546b8721a3d846e63971db4746f5d0963a10c44 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 8 Oct 2026 10:29:12 +0200 Subject: [PATCH] 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 --- kde/kate/ollie_kate.cpp | 188 +++++++++++++++++++++++++++++++++++++++- kde/kate/ollie_kate.h | 11 +++ 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/kde/kate/ollie_kate.cpp b/kde/kate/ollie_kate.cpp index 5f4846b..0b2c34e 100644 --- a/kde/kate/ollie_kate.cpp +++ b/kde/kate/ollie_kate.cpp @@ -9,13 +9,17 @@ #include #include +#include #include #include #include #include #include #include +#include +#include #include +#include #include 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(); + 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(); + 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(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(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(event); diff --git a/kde/kate/ollie_kate.h b/kde/kate/ollie_kate.h index 2e11957..b978c45 100644 --- a/kde/kate/ollie_kate.h +++ b/kde/kate/ollie_kate.h @@ -12,6 +12,7 @@ #include #include #include +#include #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 m_ghost; bool m_ghostPrefix = false; + + // Git widget integration + QPointer m_gitTreeView; }; #endif // OLLIE_KATE_PLUGIN_H