#include "olliedbusclient.h" #include #include #include #include #include #include static const char *SERVICE = "org.ollie.SessionManager"; static const char *OBJ_PATH = "/org/ollie/SessionManager"; static const char *IFACE = "org.ollie.SessionManager"; OllieDBusClient::OllieDBusClient(QObject *parent) : QObject(parent) { m_iface = new QDBusInterface(SERVICE, OBJ_PATH, IFACE, QDBusConnection::sessionBus(), this); // Watch for service appearing/disappearing auto *watcher = new QDBusServiceWatcher(SERVICE, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this); connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this]() { refreshSessions(); }); connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this]() { m_sessions.clear(); m_activeSessionId.clear(); m_chatText.clear(); m_chatOffset = 0; m_activeState = "idle"; m_activeCwd.clear(); emit sessionsChanged(); emit activeSessionIdChanged(); emit chatTextChanged(); emit activeStateChanged(); emit activeCwdChanged(); }); // Connect D-Bus signals QDBusConnection bus = QDBusConnection::sessionBus(); bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionCreated", this, SLOT(onSessionCreated(QString))); bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionKilled", this, SLOT(onSessionKilled(QString))); bus.connect(SERVICE, OBJ_PATH, IFACE, "SessionRenamed", this, SLOT(onSessionRenamed(QString,QString))); bus.connect(SERVICE, OBJ_PATH, IFACE, "StateChanged", this, SLOT(onStateChanged(QString,QString))); bus.connect(SERVICE, OBJ_PATH, IFACE, "ChatUpdated", this, SLOT(onChatUpdated(QString,qlonglong,QString))); refreshSessions(); } void OllieDBusClient::setActiveSessionId(const QString &id) { if (m_activeSessionId == id) return; m_activeSessionId = id; m_chatText.clear(); m_chatOffset = 0; emit activeSessionIdChanged(); emit chatTextChanged(); fetchChat(); fetchConfig(); } // --- Async refresh sessions --- void OllieDBusClient::refreshSessions() { if (m_refreshPending) return; // coalesce rapid-fire signals m_refreshPending = true; QDBusPendingCall pending = m_iface->asyncCall("ListSessions"); auto *watcher = new QDBusPendingCallWatcher(pending, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, &OllieDBusClient::onRefreshSessionsFinished); } void OllieDBusClient::onRefreshSessionsFinished(QDBusPendingCallWatcher *watcher) { m_refreshPending = false; watcher->deleteLater(); QDBusPendingReply reply = *watcher; if (reply.isError()) return; processSessionList(reply.value()); } void OllieDBusClient::processSessionList(const QStringList &entries) { m_sessions.clear(); for (const QString &entry : entries) { QStringList parts = entry.split('\t'); if (parts.size() >= 4) { QVariantMap session; session["id"] = parts[0]; session["state"] = parts[1]; session["model"] = parts[2]; session["agent"] = parts[3]; m_sessions.append(session); } } emit sessionsChanged(); // Update active state for (const QVariant &v : m_sessions) { QVariantMap s = v.toMap(); if (s["id"].toString() == m_activeSessionId) { QString newState = s["state"].toString(); if (newState != m_activeState) { m_activeState = newState; emit activeStateChanged(); } return; } } // Active session no longer exists if (!m_activeSessionId.isEmpty() && !m_sessions.isEmpty()) { setActiveSessionId(m_sessions.first().toMap()["id"].toString()); } else if (m_activeSessionId.isEmpty() && !m_sessions.isEmpty()) { setActiveSessionId(m_sessions.first().toMap()["id"].toString()); } else if (m_sessions.isEmpty() && !m_activeSessionId.isEmpty()) { m_activeSessionId.clear(); m_activeState = "idle"; m_chatText.clear(); m_chatOffset = 0; m_activeCwd.clear(); emit activeSessionIdChanged(); emit activeStateChanged(); emit chatTextChanged(); emit activeCwdChanged(); } } // --- Async fetch chat --- void OllieDBusClient::fetchChat() { if (m_activeSessionId.isEmpty()) return; if (m_fetchChatPending) return; // don't pile up requests m_fetchChatPending = true; QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetChat"); msg << m_activeSessionId << m_chatOffset; QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(msg); auto *watcher = new QDBusPendingCallWatcher(pending, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, &OllieDBusClient::onFetchChatFinished); } void OllieDBusClient::onFetchChatFinished(QDBusPendingCallWatcher *watcher) { m_fetchChatPending = false; watcher->deleteLater(); QDBusPendingReply reply = *watcher; if (reply.isError()) return; QString text = reply.argumentAt<0>(); qlonglong newOffset = reply.argumentAt<1>(); if (!text.isEmpty()) { if (m_chatOffset == 0) { m_chatText = text; } else { m_chatText += text; } m_chatOffset = newOffset; emit chatTextChanged(); } } // --- Async fetch config --- void OllieDBusClient::fetchConfig() { if (m_activeSessionId.isEmpty()) { if (!m_activeCwd.isEmpty()) { m_activeCwd.clear(); emit activeCwdChanged(); } return; } QDBusMessage msg = QDBusMessage::createMethodCall(SERVICE, OBJ_PATH, IFACE, "GetConfig"); msg << m_activeSessionId; QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(msg); auto *watcher = new QDBusPendingCallWatcher(pending, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, &OllieDBusClient::onFetchConfigFinished); } void OllieDBusClient::onFetchConfigFinished(QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); QDBusPendingReply reply = *watcher; if (reply.isError()) return; QString config = reply.value(); QString newCwd; const QStringList lines = config.split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { if (line.startsWith(QLatin1String("cwd="))) { newCwd = line.mid(4); break; } } if (newCwd != m_activeCwd) { m_activeCwd = newCwd; emit activeCwdChanged(); } } // --- User-initiated actions (fire-and-forget async) --- QString OllieDBusClient::createSession(const QString &cwd) { QString dir = cwd.isEmpty() ? QDir::homePath() : cwd; // Use asyncCall — we'll pick up the new session via the SessionCreated signal m_iface->asyncCall("CreateSession", dir, QString(), QString(), QString("default")); return QString(); // ID not available synchronously; signal will trigger refresh } void OllieDBusClient::createSessionWithDialog() { // Use xdg-desktop-portal FileChooser — plasmashell is QGuiApplication, // so QFileDialog (QtWidgets) cannot be used. QDBusMessage msg = QDBusMessage::createMethodCall( "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.FileChooser", "OpenFile"); QString parentWindow; // empty — no parent window handle in plasmoid context QString title = "Choose working directory for new session"; QVariantMap options; options["directory"] = true; options["modal"] = true; msg << parentWindow << title << options; QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(msg); auto *watcher = new QDBusPendingCallWatcher(pending, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) { w->deleteLater(); QDBusPendingReply reply = *w; if (reply.isError()) return; // Connect to the Response signal on the request object QString requestPath = reply.value().path(); QDBusConnection::sessionBus().connect( "org.freedesktop.portal.Desktop", requestPath, "org.freedesktop.portal.Request", "Response", this, SLOT(onPortalFolderResponse(uint,QVariantMap))); }); } void OllieDBusClient::onPortalFolderResponse(uint response, const QVariantMap &results) { if (response != 0) return; // 0 = success, 1 = cancelled, 2 = other QStringList uris = results.value("uris").toStringList(); if (uris.isEmpty()) return; // Portal returns file:// URIs QString path = QUrl(uris.first()).toLocalFile(); if (!path.isEmpty()) { createSession(path); } } bool OllieDBusClient::killSession(const QString &sessionId) { if (sessionId == m_activeSessionId) { m_activeSessionId.clear(); m_chatText.clear(); m_chatOffset = 0; m_activeState = "idle"; m_activeCwd.clear(); emit activeSessionIdChanged(); emit chatTextChanged(); emit activeStateChanged(); emit activeCwdChanged(); } m_iface->asyncCall("KillSession", sessionId); return true; } bool OllieDBusClient::renameSession(const QString &sessionId, const QString &newName) { QDBusPendingCall pending = m_iface->asyncCall("RenameSession", sessionId, newName); auto *watcher = new QDBusPendingCallWatcher(pending, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, sessionId, newName](QDBusPendingCallWatcher *w) { w->deleteLater(); QDBusPendingReply reply = *w; if (reply.isError() || !reply.value()) return; if (m_activeSessionId == sessionId) { m_activeSessionId = newName; emit activeSessionIdChanged(); } refreshSessions(); }); return true; } bool OllieDBusClient::setConfig(const QString &sessionId, const QString &key, const QString &value) { if (sessionId == m_activeSessionId && key == "cwd") { m_activeCwd = value; emit activeCwdChanged(); } m_iface->asyncCall("SetConfig", sessionId, key, value); return true; } bool OllieDBusClient::submit(const QString &sessionId, const QString &prompt) { m_iface->asyncCall("Submit", sessionId, prompt); return true; } bool OllieDBusClient::interrupt(const QString &sessionId) { m_iface->asyncCall("Interrupt", sessionId); return true; } // --- D-Bus signal handlers --- void OllieDBusClient::onSessionCreated(const QString &session_id) { Q_UNUSED(session_id); refreshSessions(); } void OllieDBusClient::onSessionKilled(const QString &session_id) { Q_UNUSED(session_id); refreshSessions(); } void OllieDBusClient::onSessionRenamed(const QString &old_id, const QString &new_id) { if (m_activeSessionId == old_id) { m_activeSessionId = new_id; emit activeSessionIdChanged(); } refreshSessions(); } void OllieDBusClient::onStateChanged(const QString &session_id, const QString &new_state) { if (session_id == m_activeSessionId) { m_activeState = new_state; emit activeStateChanged(); } refreshSessions(); } void OllieDBusClient::onChatUpdated(const QString &session_id, qlonglong offset, const QString &new_text) { if (session_id != m_activeSessionId) return; // Append directly from the signal — no need to poll if (offset == m_chatOffset) { m_chatText += new_text; m_chatOffset += new_text.toUtf8().size(); emit chatTextChanged(); } else { // Offset mismatch — do a full fetch fetchChat(); } }