#include "ollie9pclient.h" #include #include #include static QString ninepBin() { // plan9port's 9p command QString bin = QStandardPaths::findExecutable("9p"); if (!bin.isEmpty()) return bin; QString plan9 = qEnvironmentVariable("PLAN9"); if (!plan9.isEmpty() && QFileInfo::exists(plan9 + "/bin/9p")) return plan9 + "/bin/9p"; return "9p"; } static QString serverAddr() { QString ns = qEnvironmentVariable("NAMESPACE"); if (ns.isEmpty()) { // Plan 9 default: /tmp/ns.$USER.$DISPLAY QString user = qEnvironmentVariable("USER"); QString display = qEnvironmentVariable("DISPLAY"); ns = "/tmp/ns." + user + "." + display; } return "unix!" + ns + "/ollie"; } // Returns the path to ollie-9p binary, or empty string if not found. static QString ollie9pBin() { return QStandardPaths::findExecutable("ollie-9p"); } Ollie9pClient::Ollie9pClient(QObject *parent) : QObject(parent) { refreshSessions(); ensureRootDataLoaded(); startEventStream(); } Ollie9pClient::~Ollie9pClient() { stopStreams(); } void Ollie9pClient::setActiveSessionId(const QString &id) { if (m_activeSessionId == id) return; stopStreams(); m_activeSessionId = id; m_agentId.clear(); m_activeState = "idle"; if (id.isEmpty()) { emit activeSessionIdChanged(); emit activeStateChanged(); return; } // Discover agent ID BEFORE emitting signals QByteArray out = run9p({"ls", "session/" + id + "/agent"}); QString agents = QString::fromUtf8(out).trimmed(); if (!agents.isEmpty()) { m_agentId = agents.split('\n').first().trimmed(); } else { m_agentId = "0"; } // Read initial state immediately (state, not statewait) QByteArray stateOut = run9p({"read", agentPath() + "/state"}); QString stateStr = QString::fromUtf8(stateOut).trimmed(); if (!stateStr.isEmpty()) m_activeState = stateStr; emit activeSessionIdChanged(); emit activeStateChanged(); startChatStream(); startStateStream(); } void Ollie9pClient::refreshSessions() { QByteArray out = run9p({"read", "session/idx"}); QString raw = QString::fromUtf8(out).trimmed(); m_sessions.clear(); if (!raw.isEmpty()) { for (const QString &line : raw.split('\n')) { QStringList parts = line.split('\t'); if (parts.size() >= 2) { QVariantMap session; session["id"] = parts[0]; session["state"] = parts[1]; session["model"] = parts.size() > 2 ? parts[2] : ""; session["agent"] = parts.size() > 3 ? parts[3] : ""; m_sessions.append(session); } } } emit sessionsChanged(); // Auto-select first session if none active if (m_activeSessionId.isEmpty() && !m_sessions.isEmpty()) { setActiveSessionId(m_sessions.first().toMap()["id"].toString()); } } QString Ollie9pClient::readLog() { if (m_activeSessionId.isEmpty()) return {}; QByteArray out = run9p({"read", agentPath() + "/log"}); return QString::fromUtf8(out); } QString Ollie9pClient::readLogForSession(const QString &sessionId, const QString &agentId) { if (sessionId.isEmpty() || agentId.isEmpty()) return {}; QByteArray out = run9p({"read", "session/" + sessionId + "/agent/" + agentId + "/log"}); return QString::fromUtf8(out); } bool Ollie9pClient::submit(const QString &prompt) { if (m_activeSessionId.isEmpty() || prompt.trimmed().isEmpty()) return false; QProcess proc; proc.start(ninepBin(), {"-a", serverAddr(), "write", agentPath() + "/prompt"}); proc.waitForStarted(3000); proc.write(prompt.toUtf8()); proc.closeWriteChannel(); proc.waitForFinished(5000); return proc.exitCode() == 0; } bool Ollie9pClient::interrupt() { if (m_activeSessionId.isEmpty()) return false; QProcess proc; proc.start(ninepBin(), {"-a", serverAddr(), "write", agentPath() + "/ctl"}); proc.waitForStarted(3000); proc.write("stop"); proc.closeWriteChannel(); proc.waitForFinished(3000); return proc.exitCode() == 0; } bool Ollie9pClient::kill() { if (m_activeSessionId.isEmpty()) return false; QProcess proc; proc.start(ninepBin(), {"-a", serverAddr(), "write", agentPath() + "/ctl"}); proc.waitForStarted(3000); proc.write("kill"); proc.closeWriteChannel(); proc.waitForFinished(3000); return proc.exitCode() == 0; } bool Ollie9pClient::killSession(const QString &sessionId) { if (sessionId.isEmpty()) return false; QString path = "session/" + sessionId + "/ctl"; QProcess proc; proc.start(ninepBin(), {"-a", serverAddr(), "write", path}); proc.waitForStarted(3000); proc.write("kill"); proc.closeWriteChannel(); proc.waitForFinished(3000); return proc.exitCode() == 0; } QString Ollie9pClient::getConfig() { if (m_activeSessionId.isEmpty()) return {}; QByteArray out = run9p({"read", agentPath() + "/cfg"}); return QString::fromUtf8(out); } QStringList Ollie9pClient::getAgents(const QString &sessionId) { if (sessionId.isEmpty()) return {}; QByteArray out = run9p({"ls", "session/" + sessionId + "/agent"}); QString raw = QString::fromUtf8(out).trimmed(); if (raw.isEmpty()) return {"0"}; // default fallback QStringList agents = raw.split('\n', Qt::SkipEmptyParts); for (int i = 0; i < agents.size(); ++i) agents[i] = agents[i].trimmed(); return agents; } void Ollie9pClient::setActiveAgentId(const QString &agentId) { m_agentId = agentId; } void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId) { if (m_activeSessionId == sessionId && m_agentId == agentId) return; stopStreams(); m_activeSessionId = sessionId; m_agentId = agentId; m_activeState = "idle"; // Read initial state immediately QByteArray stateOut = run9p({"read", agentPath() + "/state"}); QString stateStr = QString::fromUtf8(stateOut).trimmed(); if (!stateStr.isEmpty()) m_activeState = stateStr; emit activeSessionIdChanged(); emit activeAgentIdChanged(); emit activeStateChanged(); startChatStream(); startStateStream(); } void Ollie9pClient::loadRootBackends() { if (m_rootBackendsLoaded) return; QByteArray out = run9p({"read", "backends"}); QString raw = QString::fromUtf8(out).trimmed(); if (!raw.isEmpty()) { m_availableBackends = raw.split('\n', Qt::SkipEmptyParts); m_rootBackendsLoaded = true; emit availableBackendsChanged(); } emit rootBackendsLoadedChanged(); } void Ollie9pClient::loadRootAgents() { if (m_rootAgentsLoaded) return; QByteArray out = run9p({"read", "agents"}); QString raw = QString::fromUtf8(out).trimmed(); if (!raw.isEmpty()) { m_availableAgents = raw.split('\n', Qt::SkipEmptyParts); m_rootAgentsLoaded = true; emit availableAgentsChanged(); } else { m_availableAgents = {"default"}; m_rootAgentsLoaded = true; emit availableAgentsChanged(); } emit rootAgentsLoadedChanged(); } QStringList Ollie9pClient::getAvailableModels(const QString &backend) const { if (!m_rootModelsLoaded) return {}; auto it = m_rootModels.find(backend); if (it == m_rootModels.end()) return {}; return it.value().toStringList(); } bool Ollie9pClient::createSession(const QString &cwd, const QString &name, const QString &backend, const QString &model, const QString &agent, const QString &remote) { if (cwd.isEmpty()) return false; QStringList args; args << "cwd=" + cwd; if (!name.isEmpty()) args << "name=" + name; if (!backend.isEmpty()) args << "backend=" + backend; if (!model.isEmpty()) args << "model=" + model; if (!agent.isEmpty()) args << "agent=" + agent; if (!remote.isEmpty()) args << "remote=" + remote; QProcess proc; QStringList fullArgs = {"-a", serverAddr(), "write", "session/new"}; proc.start(ninepBin(), fullArgs); proc.waitForStarted(3000); QString input = args.join(" "); proc.write(input.toUtf8()); proc.closeWriteChannel(); proc.waitForFinished(5000); if (proc.exitCode() != 0) { QString err = QString::fromUtf8(proc.readAllStandardError()).trimmed(); if (err.isEmpty()) err = QString::fromUtf8(proc.readAllStandardOutput()).trimmed(); qDebug() << "createSession failed:" << err; return false; } return true; } bool Ollie9pClient::renameSession(const QString &sessionId, const QString &newName) { if (sessionId.isEmpty() || newName.isEmpty()) return false; QString bin = ollie9pBin(); if (bin.isEmpty()) { qDebug() << "renameSession: ollie-9p not found, cannot rename"; return false; } QProcess proc; proc.setProgram(bin); proc.setArguments({"-a", serverAddr(), "mv", "session/" + sessionId, "session/" + newName}); proc.start(); proc.waitForFinished(5000); if (proc.exitCode() != 0) { qDebug() << "renameSession failed:" << QString::fromUtf8(proc.readAllStandardError()).trimmed(); return false; } // Update active session ID if we renamed the active session if (m_activeSessionId == sessionId) { m_activeSessionId = newName; emit activeSessionIdChanged(); } refreshSessions(); return true; } // --- Root data loading (lazy) --- void Ollie9pClient::ensureRootDataLoaded() { if (!m_rootBackendsLoaded) loadRootBackends(); if (!m_rootAgentsLoaded) loadRootAgents(); if (!m_rootModelsLoaded) { QByteArray out = run9p({"read", "models"}); QString raw = QString::fromUtf8(out); if (!raw.isEmpty()) { for (const QString &line : raw.split('\n')) { int tab = line.indexOf('\t'); if (tab < 0) continue; QString be = line.left(tab).trimmed(); QString mo = line.mid(tab + 1).trimmed(); if (!be.isEmpty() && !mo.isEmpty()) { QStringList list = m_rootModels[be].toStringList(); list.append(mo); m_rootModels[be] = QVariant(list); } } m_rootModelsLoaded = true; emit rootModelsLoadedChanged(); } } } // --- Streaming --- void Ollie9pClient::startChatStream() { if (m_chatProc) return; m_chatProc = new QProcess(this); connect(m_chatProc, &QProcess::readyReadStandardOutput, this, &Ollie9pClient::onChatReadyRead); connect(m_chatProc, QOverload::of(&QProcess::finished), this, &Ollie9pClient::onChatFinished); m_chatProc->start(ninepBin(), {"-a", serverAddr(), "read", agentPath() + "/chat"}); } void Ollie9pClient::startStateStream() { if (m_stateProc) return; m_stateProc = new QProcess(this); connect(m_stateProc, &QProcess::readyReadStandardOutput, this, &Ollie9pClient::onStateReadyRead); connect(m_stateProc, QOverload::of(&QProcess::finished), this, &Ollie9pClient::onStateFinished); m_stateProc->start(ninepBin(), {"-a", serverAddr(), "read", agentPath() + "/statewait"}); } void Ollie9pClient::stopStreams() { if (m_chatProc) { m_chatProc->disconnect(); m_chatProc->kill(); m_chatProc->waitForFinished(1000); m_chatProc->deleteLater(); m_chatProc = nullptr; } if (m_stateProc) { m_stateProc->disconnect(); m_stateProc->kill(); m_stateProc->waitForFinished(1000); m_stateProc->deleteLater(); m_stateProc = nullptr; } if (m_eventProc) { m_eventProc->disconnect(); m_eventProc->kill(); m_eventProc->waitForFinished(1000); m_eventProc->deleteLater(); m_eventProc = nullptr; } } void Ollie9pClient::onChatReadyRead() { QByteArray data = m_chatProc->readAllStandardOutput(); if (!data.isEmpty()) { emit chatReceived(QString::fromUtf8(data)); } } void Ollie9pClient::onStateReadyRead() { // statewait returns the state string on stdout. Parse it and update. QByteArray data = m_stateProc->readAllStandardOutput(); QString state = QString::fromUtf8(data).trimmed(); if (!state.isEmpty()) { m_activeState = state; emit activeStateChanged(); } } void Ollie9pClient::onChatFinished(int, QProcess::ExitStatus) { m_chatProc->deleteLater(); m_chatProc = nullptr; emit streamingDone(); } void Ollie9pClient::onStateFinished(int, QProcess::ExitStatus) { m_stateProc->deleteLater(); m_stateProc = nullptr; startStateStream(); } void Ollie9pClient::startEventStream() { if (m_eventProc) return; m_eventProc = new QProcess(this); connect(m_eventProc, &QProcess::readyReadStandardOutput, this, &Ollie9pClient::onEventReadyRead); connect(m_eventProc, QOverload::of(&QProcess::finished), this, &Ollie9pClient::onEventFinished); m_eventProc->start(ninepBin(), {"-a", serverAddr(), "read", "session/eventwait"}); } void Ollie9pClient::onEventReadyRead() { // Consume the data to prevent readyRead from firing again m_eventProc->readAllStandardOutput(); // Session index changed (create/delete/rename). Refresh. refreshSessions(); } void Ollie9pClient::onEventFinished(int, QProcess::ExitStatus) { m_eventProc->deleteLater(); m_eventProc = nullptr; startEventStream(); } // --- Helpers --- QString Ollie9pClient::agentPath() const { return "session/" + m_activeSessionId + "/agent/" + m_agentId; } QByteArray Ollie9pClient::run9p(const QStringList &args) { QProcess proc; QStringList fullArgs = {"-a", serverAddr()}; fullArgs.append(args); proc.start(ninepBin(), fullArgs); // Drain output while waiting. The log file can be up to 64KB; if the // parent doesn't read, the child blocks on a full pipe and never exits, // causing "QProcess: Destroyed while process is still running". QByteArray out; while (proc.state() != QProcess::NotRunning) { if (proc.waitForReadyRead(100)) out += proc.readAllStandardOutput(); } out += proc.readAllStandardOutput(); return out; }