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:<verb>' 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.
This commit is contained in:
Levi Neely 2026-10-09 10:54:47 +02:00
parent 36e9890ec7
commit 67e8d53827
3 changed files with 96 additions and 0 deletions

View File

@ -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

View File

@ -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.

View File

@ -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.