#include "olliedbusclient.h" #include #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(); m_activeModel.clear(); m_activeBackend.clear(); emit sessionsChanged(); emit activeSessionIdChanged(); emit chatTextChanged(); emit activeStateChanged(); emit activeCwdChanged(); emit activeModelChanged(); emit activeBackendChanged(); }); // 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; // Update state from sessions list QString newState = "idle"; for (const QVariant &s : m_sessions) { QVariantMap session = s.toMap(); if (session["id"].toString() == id) { newState = session["state"].toString(); break; } } if (newState != m_activeState) { m_activeState = newState; emit activeStateChanged(); } 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); } } std::sort(m_sessions.begin(), m_sessions.end(), [](const QVariant &a, const QVariant &b) { return a.toMap()["id"].toString().compare(b.toMap()["id"].toString(), Qt::CaseInsensitive) < 0; }); 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(); m_activeModel.clear(); m_activeBackend.clear(); emit activeSessionIdChanged(); emit activeStateChanged(); emit chatTextChanged(); emit activeCwdChanged(); emit activeModelChanged(); emit activeBackendChanged(); } } // --- 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, newModel, newBackend; const QStringList lines = config.split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { if (line.startsWith(QLatin1String("cwd="))) newCwd = line.mid(4); else if (line.startsWith(QLatin1String("model="))) newModel = line.mid(6); else if (line.startsWith(QLatin1String("backend="))) newBackend = line.mid(8); } if (newCwd != m_activeCwd) { m_activeCwd = newCwd; emit activeCwdChanged(); } if (newModel != m_activeModel) { m_activeModel = newModel; emit activeModelChanged(); } if (newBackend != m_activeBackend) { m_activeBackend = newBackend; emit activeBackendChanged(); } } // --- 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. // // We use handle_token to make the request object path predictable, then // connect to the Response signal BEFORE making the call. This avoids a // race where the portal fires Response before we subscribe to the signal. static int handleSeq = 0; QString handleToken = QString("ollie_folder_%1").arg(++handleSeq); // Predict the request object path: /org/freedesktop/portal/desktop/request// QString sender = QDBusConnection::sessionBus().baseService(); sender.replace('.', '_'); if (sender.startsWith(':')) sender = sender.mid(1); QString requestPath = QString("/org/freedesktop/portal/desktop/request/%1/%2") .arg(sender, handleToken); // Connect to Response BEFORE making the call to avoid race QDBusConnection::sessionBus().connect( "org.freedesktop.portal.Desktop", requestPath, "org.freedesktop.portal.Request", "Response", this, SLOT(onPortalFolderResponse(uint,QVariantMap))); 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; options["handle_token"] = handleToken; msg << parentWindow << title << options; QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(msg); auto *watcher = new QDBusPendingCallWatcher(pending, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, requestPath](QDBusPendingCallWatcher *w) { w->deleteLater(); QDBusPendingReply reply = *w; if (reply.isError()) { // Clean up the signal connection on failure QDBusConnection::sessionBus().disconnect( "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); } } QStringList OllieDBusClient::peerList(const QString &sessionId) { QDBusReply reply = m_iface->call("PeerList", sessionId); if (reply.isValid()) return reply.value(); return {}; } QVariantMap OllieDBusClient::getChat(const QString &sessionId, int offset) { QDBusMessage msg = QDBusMessage::createMethodCall( "org.ollie.SessionManager", "/org/ollie/SessionManager", "org.ollie.SessionManager", "GetChat"); msg << sessionId << (qlonglong)offset; QDBusMessage reply = QDBusConnection::sessionBus().call(msg); QVariantMap result; if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().size() >= 2) { result["text"] = reply.arguments()[0].toString(); result["offset"] = reply.arguments()[1].toInt(); } return result; } QVariantMap OllieDBusClient::getConfig(const QString &sessionId) { QDBusReply reply = m_iface->call("GetConfig", sessionId); QVariantMap result; if (reply.isValid()) { for (const QString &line : reply.value().split('\n')) { int eq = line.indexOf('='); if (eq > 0) result[line.left(eq)] = line.mid(eq + 1); } } return result; } QString OllieDBusClient::getState(const QString &sessionId) { QDBusReply reply = m_iface->call("GetState", sessionId); if (reply.isValid()) return reply.value(); return "idle"; } 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(); } // Update state in-place without emitting sessionsChanged() — that would // cause the ListView to recreate all delegates, breaking inline rename. // The QML state indicator for the active session uses dbus.activeState // (updated above). For other sessions the dot updates on the next real // refresh (session created/killed/renamed). for (int i = 0; i < m_sessions.size(); ++i) { QVariantMap session = m_sessions[i].toMap(); if (session["id"].toString() == session_id) { session["state"] = new_state; m_sessions[i] = session; emit sessionStateUpdated(session_id, new_state); return; } } } void OllieDBusClient::onChatUpdated(const QString &session_id, qlonglong offset, const QString &new_text) { // Emit for all panes listening to any session emit chatAppended(session_id, static_cast(offset), 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(); } }