#include "ollie9pclient.h" #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"; } Ollie9pClient::Ollie9pClient(QObject *parent) : QObject(parent) { refreshSessions(); } 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); } 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; } 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(); } // --- 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; } } 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(); } // --- 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; }