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 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 the actions live), so it appears only when a language server is backing the
buffer and is hidden entirely otherwise. 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, - **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 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 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()) { if (RadialNode lsp = lspBranchIfActive(); !lsp.children.isEmpty()) {
root.children.push_back(lsp); root.children.push_back(lsp);
} }
if (RadialNode ollie = ollieBranchIfActive(); !ollie.children.isEmpty()) {
root.children.push_back(ollie);
}
} }
m_radial->popup(root, globalPos); m_radial->popup(root, globalPos);
@ -199,8 +202,87 @@ RadialNode RadialView::lspBranchIfActive() const
return branch; 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) 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 // Launcher doors (palette / switchers) are reachable from inside a radial as
// special slices. Sibling deft plugins register these actions on the window; // special slices. Sibling deft plugins register these actions on the window;
// trigger them by objectName so no cross-plugin link is needed. // 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. // the right-button editing radial at show time.
RadialNode lspBranchIfActive() const; 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 // Find a Kate action by its raw objectName (then visible text) across all
// GUI clients. Resolves radial-slice ids, including the launcher actions // GUI clients. Resolves radial-slice ids, including the launcher actions
// that sibling deft plugins register on the window. // that sibling deft plugins register on the window.