This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-kde/gui/ollie9pclient.cpp

482 lines
14 KiB
C++

#include "ollie9pclient.h"
#include <QDir>
#include <QStandardPaths>
#include <QDebug>
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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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<int, QProcess::ExitStatus>::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;
}