429 lines
13 KiB
C++
429 lines
13 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";
|
|
}
|
|
|
|
Ollie9pClient::Ollie9pClient(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
refreshSessions();
|
|
ensureRootDataLoaded();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
// Use ollie-9p mv (which uses 9P Twstat to rename the session directory).
|
|
QString bin = QStandardPaths::findExecutable("ollie-9p");
|
|
if (bin.isEmpty()) {
|
|
qDebug() << "renameSession: ollie-9p not found, cannot rename";
|
|
return false;
|
|
}
|
|
|
|
QProcess proc;
|
|
proc.start(bin, {"-a", serverAddr(), "mv", "session/" + sessionId, "session/" + newName});
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|