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

242 lines
6.7 KiB
C++

#include "ollie9pclient.h"
#include <QDir>
#include <QStandardPaths>
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";
emit activeSessionIdChanged();
emit activeStateChanged();
if (id.isEmpty()) return;
// Discover agent ID
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";
}
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);
}
// --- 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;
}
}
void Ollie9pClient::onChatReadyRead()
{
QByteArray data = m_chatProc->readAllStandardOutput();
if (!data.isEmpty()) {
emit chatReceived(QString::fromUtf8(data));
}
}
void Ollie9pClient::onStateReadyRead()
{
QByteArray data = m_stateProc->readAllStandardOutput();
QString state = QString::fromUtf8(data).trimmed();
if (!state.isEmpty()) {
// statewait returns one line per state change; take the last one
QStringList lines = state.split('\n', Qt::SkipEmptyParts);
if (!lines.isEmpty()) {
m_activeState = lines.last().trimmed();
emit activeStateChanged();
}
}
// statewait exits after one read — restart it for continuous monitoring
m_stateProc->deleteLater();
m_stateProc = nullptr;
startStateStream();
}
void Ollie9pClient::onChatFinished(int, QProcess::ExitStatus)
{
// Chat stream ended — session was killed or connection lost
m_chatProc->deleteLater();
m_chatProc = nullptr;
emit streamingDone();
}
void Ollie9pClient::onStateFinished(int, QProcess::ExitStatus)
{
// State process exited unexpectedly (not via onStateReadyRead restart)
if (m_stateProc) {
m_stateProc->deleteLater();
m_stateProc = nullptr;
}
}
// --- 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);
proc.waitForFinished(10000);
return proc.readAllStandardOutput();
}