From 67e8d538270a93345f6668bbdfcb2ab942bc1254 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 9 Oct 2026 10:54:47 +0200 Subject: [PATCH] radial: add a conditional Ollie submenu to the right-button radial When the sibling Ollie Kate plugin (ollie-kate) is loaded in the window, the right-button radial grows an 'Ollie' branch: Ask, Explain, Fix, Refactor, Tests, Doc, Verbatim, Start. Ollie's context actions are not window/collection actions (they live on a transient context menu, which the radial suppresses anyway), so findActionByName cannot resolve them. Instead each slice carries an 'ollie:' id; triggerOllieVerb() resolves the plugin view via pluginView("ollie-kate") and invokes its Q_INVOKABLE action method by name (submitSelectionPrompt for fix/refactor/test/doc, the zero-arg methods for the rest). The branch is built at show time and omitted when the plugin is not loaded, mirroring the LSP branch. 18/18 ctest green. --- README.md | 4 ++ src/radial/radialplugin.cpp | 82 +++++++++++++++++++++++++++++++++++++ src/radial/radialplugin.h | 10 +++++ 3 files changed, 96 insertions(+) diff --git a/README.md b/README.md index 209d82a..1df8afc 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,10 @@ through a list. Format, Code Action, Symbol Info, Hover. It is decided at open time (resolving the actions live), so it appears only when a language server is backing the buffer and is hidden entirely otherwise. +- **Ollie branch (conditional).** When the **Ollie** Kate plugin (the sibling + AI-agent plugin) is enabled, the right-button radial grows an **Ollie** + submenu — Ask, Explain, Fix, Refactor, Tests, Doc, Verbatim, Start — invoking + Ollie's context actions. Shown only while that plugin is loaded. - **Pointer follows focus.** When a slice changes the active pane (split, close, or focus-move), the pointer is warped to the centre of the new pane so the mouse stays where your attention is — the same cursor-follows-focus feel as diff --git a/src/radial/radialplugin.cpp b/src/radial/radialplugin.cpp index 1d6ca37..11a53bd 100644 --- a/src/radial/radialplugin.cpp +++ b/src/radial/radialplugin.cpp @@ -156,6 +156,9 @@ void RadialView::showRadialAt(int defIndex, const QPoint &globalPos) if (RadialNode lsp = lspBranchIfActive(); !lsp.children.isEmpty()) { root.children.push_back(lsp); } + if (RadialNode ollie = ollieBranchIfActive(); !ollie.children.isEmpty()) { + root.children.push_back(ollie); + } } m_radial->popup(root, globalPos); @@ -199,8 +202,87 @@ RadialNode RadialView::lspBranchIfActive() const return branch; } +RadialNode RadialView::ollieBranchIfActive() const +{ + // Only show the Ollie branch when the Ollie Kate plugin is loaded in this + // window. Its context actions are not window/collection actions (they live + // on a transient context menu), so we cannot resolve them via + // findActionByName; instead we invoke the plugin view's Q_INVOKABLE action + // methods by name in onRadialActivated. Each slice carries an "ollie:" id. + RadialNode branch; + if (!m_mainWindow->pluginView(QStringLiteral("ollie-kate"))) { + return branch; // not enabled -> empty, caller omits it + } + + auto leaf = [](const QString &label, const QString &id) { + RadialNode n; + n.label = label; + n.actionId = id; + return n; + }; + + branch.label = QStringLiteral("Ollie"); + branch.children = { + leaf(QStringLiteral("Ask"), QStringLiteral("ollie:ask")), + leaf(QStringLiteral("Explain"), QStringLiteral("ollie:explain")), + leaf(QStringLiteral("Fix"), QStringLiteral("ollie:fix")), + leaf(QStringLiteral("Refactor"), QStringLiteral("ollie:refactor")), + leaf(QStringLiteral("Tests"), QStringLiteral("ollie:test")), + leaf(QStringLiteral("Doc"), QStringLiteral("ollie:doc")), + leaf(QStringLiteral("Verbatim"), QStringLiteral("ollie:verbatim")), + leaf(QStringLiteral("Start"), QStringLiteral("ollie:start")), + }; + return branch; +} + +// Dispatch an "ollie:" radial slice to the Ollie Kate plugin's view by invoking +// its Q_INVOKABLE action method. Returns true if the id was an Ollie verb (even +// if the plugin is momentarily unavailable), so the caller does not also try to +// resolve it as a Kate action. +bool RadialView::triggerOllieVerb(const QString &actionId) const +{ + if (!actionId.startsWith(QLatin1String("ollie:"))) { + return false; + } + QObject *view = m_mainWindow->pluginView(QStringLiteral("ollie-kate")); + if (!view) { + return true; // recognised as ours, but nothing to invoke + } + const QString verb = actionId.mid(QStringLiteral("ollie:").size()); + + // The selection-prompt verbs share one method with an argument; the rest are + // zero-argument methods matching the Q_INVOKABLE names on the Ollie view. + if (verb == QLatin1String("fix")) { + QMetaObject::invokeMethod(view, "submitSelectionPrompt", + Q_ARG(QString, QStringLiteral("Fix"))); + } else if (verb == QLatin1String("refactor")) { + QMetaObject::invokeMethod(view, "submitSelectionPrompt", + Q_ARG(QString, QStringLiteral("Refactor"))); + } else if (verb == QLatin1String("test")) { + QMetaObject::invokeMethod(view, "submitSelectionPrompt", + Q_ARG(QString, QStringLiteral("Write unit tests for"))); + } else if (verb == QLatin1String("doc")) { + QMetaObject::invokeMethod(view, "submitSelectionPrompt", + Q_ARG(QString, QStringLiteral("Add documentation comments to"))); + } else if (verb == QLatin1String("ask")) { + QMetaObject::invokeMethod(view, "askAboutFile"); + } else if (verb == QLatin1String("explain")) { + QMetaObject::invokeMethod(view, "explainSelection"); + } else if (verb == QLatin1String("verbatim")) { + QMetaObject::invokeMethod(view, "sendVerbatim"); + } else if (verb == QLatin1String("start")) { + QMetaObject::invokeMethod(view, "startSession"); + } + return true; +} + void RadialView::onRadialActivated(const QString &actionId) { + // Ollie verbs are not Kate actions; dispatch them to the Ollie plugin view. + if (triggerOllieVerb(actionId)) { + return; + } + // Launcher doors (palette / switchers) are reachable from inside a radial as // special slices. Sibling deft plugins register these actions on the window; // trigger them by objectName so no cross-plugin link is needed. diff --git a/src/radial/radialplugin.h b/src/radial/radialplugin.h index 45a92a2..7d40b9d 100644 --- a/src/radial/radialplugin.h +++ b/src/radial/radialplugin.h @@ -70,6 +70,16 @@ private: // the right-button editing radial at show time. RadialNode lspBranchIfActive() const; + // Build an "Ollie" submenu of the Ollie Kate plugin's context actions when + // that plugin is loaded in this window; empty otherwise so the caller omits + // it. Spliced into the right-button editing radial at show time. + RadialNode ollieBranchIfActive() const; + + // Dispatch an "ollie:"-prefixed slice id to the Ollie plugin view's + // Q_INVOKABLE action method. Returns true if the id was an Ollie verb, so + // onRadialActivated does not also treat it as a Kate action. + bool triggerOllieVerb(const QString &actionId) const; + // Find a Kate action by its raw objectName (then visible text) across all // GUI clients. Resolves radial-slice ids, including the launcher actions // that sibling deft plugins register on the window.