phase 5: KRunner plugin
- 'ollie <prompt>': one-shot mode (create, submit, await, notify, kill) - 'ollie list': show active sessions with state - 'ollie kill <id>': terminate a session - KNotification shows result when one-shot completes - Installs to /usr/lib64/qt6/plugins/kf6/krunner/
This commit is contained in:
parent
f4b5ce3ea0
commit
e052424d62
|
|
@ -10,9 +10,13 @@ set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
|||
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core DBus Widgets Quick)
|
||||
find_package(KF6StatusNotifierItem REQUIRED)
|
||||
find_package(KF6 REQUIRED COMPONENTS Runner Notifications CoreAddons Config)
|
||||
find_package(Plasma REQUIRED)
|
||||
|
||||
# libollie.so built by Go (expected in build/ after `make lib`)
|
||||
|
|
@ -57,6 +61,16 @@ target_link_libraries(ollie-tray
|
|||
KF6::StatusNotifierItem
|
||||
)
|
||||
|
||||
# --- KRunner Plugin ---
|
||||
|
||||
kcoreaddons_add_plugin(krunner_ollie SOURCES krunner/ollie_runner.cpp INSTALL_NAMESPACE "kf6/krunner")
|
||||
target_link_libraries(krunner_ollie
|
||||
KF6::Runner
|
||||
Qt6::Core
|
||||
Qt6::DBus
|
||||
Qt6::Widgets
|
||||
)
|
||||
|
||||
# --- Plasmoid QML Plugin ---
|
||||
|
||||
qt6_add_qml_module(ollie-plasmoid-plugin
|
||||
|
|
|
|||
7
Makefile
7
Makefile
|
|
@ -27,6 +27,13 @@ run: daemon
|
|||
tray: daemon
|
||||
./$(BUILD_DIR)/ollie-tray
|
||||
|
||||
# Install KRunner plugin
|
||||
krunner-install: daemon
|
||||
sudo cp $(BUILD_DIR)/lib/kf6/krunner/krunner_ollie.so \
|
||||
/usr/lib64/qt6/plugins/kf6/krunner/
|
||||
kquitapp6 krunner 2>/dev/null; true
|
||||
@echo "KRunner plugin installed. Restart krunner or re-login."
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
rm -f $(LIB_DIR)/libollie.so $(LIB_DIR)/libollie.h
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"Id": "org.ollie.krunner",
|
||||
"Name": "Ollie",
|
||||
"Description": "AI agent prompts and session management",
|
||||
"Icon": "system-run",
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "ollie-kde"
|
||||
}
|
||||
]
|
||||
},
|
||||
"X-Plasma-API-Minimum-Version": "6.0",
|
||||
"X-KDE-PluginInfo-EnabledByDefault": true
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
#include "ollie_runner.h"
|
||||
|
||||
#include <KPluginFactory>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDir>
|
||||
#include <QThread>
|
||||
#include <QVariantMap>
|
||||
#include <QDialog>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
#include <QLabel>
|
||||
|
||||
K_PLUGIN_CLASS_WITH_JSON(OllieRunner, "metadata.json")
|
||||
|
||||
static const char *SERVICE = "org.ollie.SessionManager";
|
||||
static const char *OBJ_PATH = "/org/ollie/SessionManager";
|
||||
static const char *IFACE = "org.ollie.SessionManager";
|
||||
|
||||
OllieRunner::OllieRunner(QObject *parent, const KPluginMetaData &data)
|
||||
: AbstractRunner(parent, data)
|
||||
{
|
||||
m_iface = new QDBusInterface(SERVICE, OBJ_PATH, IFACE,
|
||||
QDBusConnection::sessionBus(), this);
|
||||
}
|
||||
|
||||
void OllieRunner::match(KRunner::RunnerContext &context)
|
||||
{
|
||||
const QString query = context.query().trimmed();
|
||||
|
||||
// Only activate on "ollie" prefix
|
||||
if (!query.startsWith(QLatin1String("ollie ")) && query != QLatin1String("ollie")) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString args = query.mid(6).trimmed(); // everything after "ollie "
|
||||
|
||||
if (args.isEmpty() || args == QLatin1String("list")) {
|
||||
// List sessions
|
||||
QStringList sessions = listSessions();
|
||||
if (sessions.isEmpty()) {
|
||||
KRunner::QueryMatch match(this);
|
||||
match.setId("ollie-no-sessions");
|
||||
match.setText("No active sessions");
|
||||
match.setIconName("dialog-information");
|
||||
match.setRelevance(0.5);
|
||||
context.addMatch(match);
|
||||
} else {
|
||||
for (const QString &entry : sessions) {
|
||||
QStringList parts = entry.split('\t');
|
||||
if (parts.size() < 4) continue;
|
||||
|
||||
KRunner::QueryMatch match(this);
|
||||
match.setId("ollie-session-" + parts[0]);
|
||||
QString state = parts[1] == "idle" ? "○" : "●";
|
||||
match.setText(state + " " + parts[0].left(14) + " [" + parts[3] + "]");
|
||||
match.setSubtext(parts[2] + " — " + parts[1]);
|
||||
match.setIconName("system-run");
|
||||
match.setRelevance(0.7);
|
||||
match.setData(parts[0]); // session ID
|
||||
context.addMatch(match);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.startsWith(QLatin1String("kill "))) {
|
||||
QString target = args.mid(5).trimmed();
|
||||
KRunner::QueryMatch match(this);
|
||||
match.setId("ollie-kill-" + target);
|
||||
match.setText("Kill session: " + target);
|
||||
match.setSubtext("Terminate ollie session");
|
||||
match.setIconName("process-stop");
|
||||
match.setRelevance(0.8);
|
||||
match.setData("kill:" + target);
|
||||
context.addMatch(match);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: one-shot prompt
|
||||
KRunner::QueryMatch match(this);
|
||||
match.setId("ollie-prompt");
|
||||
match.setText("Ask Ollie: " + args);
|
||||
match.setSubtext("One-shot prompt — result via notification");
|
||||
match.setIconName("system-run");
|
||||
match.setRelevance(0.9);
|
||||
match.setData("prompt:" + args);
|
||||
context.addMatch(match);
|
||||
}
|
||||
|
||||
void OllieRunner::run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match)
|
||||
{
|
||||
Q_UNUSED(context);
|
||||
QString data = match.data().toString();
|
||||
|
||||
if (data.startsWith("kill:")) {
|
||||
QString id = data.mid(5);
|
||||
killSession(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.startsWith("prompt:")) {
|
||||
QString prompt = data.mid(7);
|
||||
QString result = oneShotRun(prompt);
|
||||
|
||||
// Show popup window with result
|
||||
auto *dialog = new QDialog(nullptr);
|
||||
dialog->setWindowTitle("Ollie — " + prompt.left(40));
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->resize(520, 380);
|
||||
|
||||
auto *layout = new QVBoxLayout(dialog);
|
||||
|
||||
auto *textEdit = new QTextEdit(dialog);
|
||||
textEdit->setReadOnly(true);
|
||||
textEdit->setPlainText(result);
|
||||
textEdit->setFont(QFont("monospace", 11));
|
||||
layout->addWidget(textEdit);
|
||||
|
||||
auto *buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->addStretch();
|
||||
|
||||
auto *copyBtn = new QPushButton("Copy", dialog);
|
||||
QObject::connect(copyBtn, &QPushButton::clicked, [result]() {
|
||||
QApplication::clipboard()->setText(result);
|
||||
});
|
||||
buttonLayout->addWidget(copyBtn);
|
||||
|
||||
auto *closeBtn = new QPushButton("Close", dialog);
|
||||
QObject::connect(closeBtn, &QPushButton::clicked, dialog, &QDialog::close);
|
||||
buttonLayout->addWidget(closeBtn);
|
||||
|
||||
layout->addLayout(buttonLayout);
|
||||
|
||||
dialog->show();
|
||||
dialog->raise();
|
||||
dialog->activateWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clicked a session from list — not much to do in KRunner
|
||||
// Could open the plasmoid in the future
|
||||
}
|
||||
|
||||
// --- D-Bus helpers ---
|
||||
|
||||
QString OllieRunner::oneShotRun(const QString &prompt)
|
||||
{
|
||||
// 1. Create session
|
||||
QDBusReply<QString> createReply = m_iface->call(
|
||||
"CreateSession", QDir::homePath(), QString(), QString(), "default");
|
||||
if (!createReply.isValid()) {
|
||||
return "Error: could not create session";
|
||||
}
|
||||
QString sessionId = createReply.value();
|
||||
|
||||
// 2. Submit prompt
|
||||
QDBusReply<bool> submitReply = m_iface->call("Submit", sessionId, prompt);
|
||||
if (!submitReply.isValid() || !submitReply.value()) {
|
||||
killSession(sessionId);
|
||||
return "Error: could not submit prompt";
|
||||
}
|
||||
|
||||
// 3. Poll for completion (max 30s)
|
||||
QString state = "thinking";
|
||||
for (int i = 0; i < 60; i++) {
|
||||
QThread::msleep(500);
|
||||
|
||||
QDBusReply<QString> stateReply = m_iface->call("GetState", sessionId);
|
||||
if (!stateReply.isValid()) break;
|
||||
state = stateReply.value();
|
||||
if (state == "idle") break;
|
||||
}
|
||||
|
||||
// 4. Get the chat output
|
||||
QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetChat");
|
||||
msg << sessionId << (qlonglong)0;
|
||||
QDBusMessage reply = QDBusConnection::sessionBus().call(msg);
|
||||
|
||||
QString chatText;
|
||||
if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() >= 1) {
|
||||
chatText = reply.arguments()[0].toString();
|
||||
}
|
||||
|
||||
// 5. Extract assistant response (text between [assistant]\n and next \n\n[)
|
||||
QString result;
|
||||
int aStart = chatText.indexOf("[assistant]\n");
|
||||
if (aStart >= 0) {
|
||||
aStart += 12; // skip "[assistant]\n"
|
||||
int aEnd = chatText.indexOf("\n\n[", aStart);
|
||||
if (aEnd < 0) aEnd = chatText.length();
|
||||
result = chatText.mid(aStart, aEnd - aStart).trimmed();
|
||||
} else {
|
||||
result = chatText; // fallback: show everything
|
||||
}
|
||||
|
||||
// 6. Kill session
|
||||
killSession(sessionId);
|
||||
|
||||
return result.isEmpty() ? "(no response)" : result;
|
||||
}
|
||||
|
||||
QStringList OllieRunner::listSessions()
|
||||
{
|
||||
QDBusReply<QStringList> reply = m_iface->call("ListSessions");
|
||||
if (reply.isValid()) return reply.value();
|
||||
return {};
|
||||
}
|
||||
|
||||
void OllieRunner::killSession(const QString &id)
|
||||
{
|
||||
m_iface->call("KillSession", id);
|
||||
}
|
||||
|
||||
#include "ollie_runner.moc"
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#ifndef OLLIE_RUNNER_H
|
||||
#define OLLIE_RUNNER_H
|
||||
|
||||
#include <KRunner/AbstractRunner>
|
||||
#include <KRunner/RunnerContext>
|
||||
#include <KRunner/QueryMatch>
|
||||
#include <QDBusInterface>
|
||||
|
||||
class OllieRunner : public KRunner::AbstractRunner
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OllieRunner(QObject *parent, const KPluginMetaData &data);
|
||||
|
||||
void match(KRunner::RunnerContext &context) override;
|
||||
void run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match) override;
|
||||
|
||||
private:
|
||||
QDBusInterface *m_iface;
|
||||
|
||||
QString oneShotRun(const QString &prompt);
|
||||
QStringList listSessions();
|
||||
void killSession(const QString &id);
|
||||
};
|
||||
|
||||
#endif // OLLIE_RUNNER_H
|
||||
Reference in New Issue