From a4c659f5913c711488f27f9ffe897dc2f0e2df0b Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 9 Oct 2026 10:51:35 +0200 Subject: [PATCH] kate: add Ollie context menu to project tree view Right-click on files/directories in Kate's project panel now shows Ollie actions: For directories: - Ask about directory (explain purpose/structure) - Start Session Here For files: - Ask about file (explain purpose/design) - Review file (code review) - Write tests Constructs file paths by walking up the tree model and joining with the project base directory. --- kde/kate/ollie_kate.cpp | 137 ++++++++++++++++++++++++++++++++++++++++ kde/kate/ollie_kate.h | 3 + 2 files changed, 140 insertions(+) diff --git a/kde/kate/ollie_kate.cpp b/kde/kate/ollie_kate.cpp index 3a5ed84..277900e 100644 --- a/kde/kate/ollie_kate.cpp +++ b/kde/kate/ollie_kate.cpp @@ -172,6 +172,9 @@ OllieKateView::OllieKateView(OllieKatePlugin *plugin, KTextEditor::MainWindow *m if (auto *view = mainWindow->activeView()) setupContextMenu(view); + // Project tree context menu + setupProjectTreeContextMenu(); + // Git++ "Review Diff" action (third-party plugin) setupGitPlusPlusDiffAction(); @@ -542,6 +545,140 @@ void OllieKateView::onGitTreeContextMenu(const QPoint &) // Unused - context menu integration not possible without Kate modification } +void OllieKateView::setupProjectTreeContextMenu() +{ + // Find the project tree view — it's inside the kateproject tool view + QWidget *projectToolView = m_mainWindow->window()->findChild(QStringLiteral("kateproject")); + + if (!projectToolView) { + // Schedule retry — plugin view might not be created yet + QTimer::singleShot(2000, this, &OllieKateView::setupProjectTreeContextMenu); + return; + } + + // Find the tree view inside the project tool view + auto *treeView = projectToolView->findChild(); + if (!treeView) { + QTimer::singleShot(2000, this, &OllieKateView::setupProjectTreeContextMenu); + return; + } + + // Avoid duplicate setup + if (m_projectTreeView == treeView) { + return; + } + m_projectTreeView = treeView; + + // Enable custom context menu + treeView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(treeView, &QTreeView::customContextMenuRequested, + this, &OllieKateView::onProjectTreeContextMenu); +} + +void OllieKateView::onProjectTreeContextMenu(const QPoint &pos) +{ + if (!m_projectTreeView) return; + + auto *model = m_projectTreeView->model(); + if (!model) return; + + QModelIndex index = m_projectTreeView->indexAt(pos); + if (!index.isValid()) return; + + // Get the file path from the model + // Kate's project model stores the path in UserRole or as a property + QString filePath = model->data(index, Qt::UserRole).toString(); + if (filePath.isEmpty()) { + // Try DisplayRole as fallback and construct path + QString name = model->data(index, Qt::DisplayRole).toString(); + QString baseDir = projectBaseDir(); + if (!baseDir.isEmpty() && !name.isEmpty()) { + // Walk up the tree to construct full path + QStringList parts; + parts.prepend(name); + QModelIndex parent = index.parent(); + while (parent.isValid()) { + parts.prepend(model->data(parent, Qt::DisplayRole).toString()); + parent = parent.parent(); + } + // Remove the project root name (first element) and join + if (!parts.isEmpty()) parts.removeFirst(); + filePath = baseDir + QStringLiteral("/") + parts.join(QStringLiteral("/")); + } + } + + if (filePath.isEmpty()) return; + + QFileInfo fi(filePath); + bool isDir = fi.isDir(); + + QMenu menu(m_projectTreeView); + + if (isDir) { + auto *askAction = menu.addAction(QIcon::fromTheme(QStringLiteral("help-about")), + QStringLiteral("Ollie: Ask about directory")); + connect(askAction, &QAction::triggered, this, [this, filePath]() { + ensureAgent(); + QString baseDir = projectBaseDir(); + QString relPath = filePath; + if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) { + relPath = filePath.mid(baseDir.length() + 1); + } + QString prompt = QStringLiteral("Explain the purpose and structure of the directory: ") + relPath; + m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8()); + }); + + auto *startAction = menu.addAction(QIcon::fromTheme(QStringLiteral("system-run")), + QStringLiteral("Ollie: Start Session Here")); + connect(startAction, &QAction::triggered, this, [this, filePath]() { + ensureAgent(); + QString prompt = QStringLiteral("I want to work on code in: ") + filePath; + m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8()); + }); + } else { + auto *askAction = menu.addAction(QIcon::fromTheme(QStringLiteral("help-about")), + QStringLiteral("Ollie: Ask about file")); + connect(askAction, &QAction::triggered, this, [this, filePath]() { + ensureAgent(); + QString baseDir = projectBaseDir(); + QString relPath = filePath; + if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) { + relPath = filePath.mid(baseDir.length() + 1); + } + QString prompt = QStringLiteral("Explain the purpose and design of the file: ") + relPath; + m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8()); + }); + + auto *reviewAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-preview")), + QStringLiteral("Ollie: Review file")); + connect(reviewAction, &QAction::triggered, this, [this, filePath]() { + ensureAgent(); + QString baseDir = projectBaseDir(); + QString relPath = filePath; + if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) { + relPath = filePath.mid(baseDir.length() + 1); + } + QString prompt = QStringLiteral("Review the code in ") + relPath + QStringLiteral(" for bugs, issues, and improvements."); + m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8()); + }); + + auto *testAction = menu.addAction(QIcon::fromTheme(QStringLiteral("system-run")), + QStringLiteral("Ollie: Write tests")); + connect(testAction, &QAction::triggered, this, [this, filePath]() { + ensureAgent(); + QString baseDir = projectBaseDir(); + QString relPath = filePath; + if (!baseDir.isEmpty() && filePath.startsWith(baseDir)) { + relPath = filePath.mid(baseDir.length() + 1); + } + QString prompt = QStringLiteral("Write tests for the code in ") + relPath; + m_9p->write(agentPath() + QStringLiteral("/prompt"), prompt.toUtf8()); + }); + } + + menu.exec(m_projectTreeView->viewport()->mapToGlobal(pos)); +} + void OllieKateView::onWidgetAdded(QWidget *widget) { // Check if this is a DiffWidget by looking for characteristic properties diff --git a/kde/kate/ollie_kate.h b/kde/kate/ollie_kate.h index 68d3502..f27b36a 100644 --- a/kde/kate/ollie_kate.h +++ b/kde/kate/ollie_kate.h @@ -91,9 +91,11 @@ private: // Context menu void setupContextMenu(KTextEditor::View *view); + void setupProjectTreeContextMenu(); void setupGitPlusPlusDiffAction(); void setupProjectGitWidget(); void onDiffWidgetContextMenu(QWidget *diffWidget, const QPoint &pos); + void onProjectTreeContextMenu(const QPoint &pos); // Ghost text void setupGhostProvider(); @@ -113,6 +115,7 @@ private: // Git widget integration QPointer m_gitTreeView; + QPointer m_projectTreeView; }; #endif // OLLIE_KATE_PLUGIN_H