fix connection dot indicators

This commit is contained in:
Levi Neely 2026-08-02 23:59:28 +02:00
parent 41f95e84c2
commit 4dabab1f71
5 changed files with 53 additions and 15 deletions

View File

@ -228,6 +228,10 @@ ApplicationWindow {
}
scrollTimer.start()
}
function onSessionConnected() {
chatModel.appendData("\n[[[info]]]\nsession connected\n[[[end]]]\n")
scrollTimer.start()
}
function onStreamingDone() {
chatModel.appendData("\n[[[info]]]\nsession ended\n[[[end]]]\n")
}

View File

@ -19,6 +19,7 @@ void NinePConnection::start(const QString &program, const QStringList &args, int
m_retryMs = retryMs;
m_probe = probe;
m_stopRequested = false;
m_openMarkerSeen = false;
m_retryTimer.stop();
if (!m_process)
startProcess();
@ -43,6 +44,7 @@ void NinePConnection::startProcess()
if (m_stopRequested || m_process)
return;
m_stderr.clear();
m_openMarkerSeen = false;
m_process = new QProcess(this);
connect(m_process, &QProcess::started, this, &NinePConnection::onStarted);
connect(m_process, &QProcess::readyReadStandardOutput, this, &NinePConnection::onStdout);
@ -77,8 +79,10 @@ void NinePConnection::onStderr()
if (end < 0) break;
const QByteArray line = m_stderr.left(end).trimmed();
m_stderr.remove(0, end + 1);
if (line == "OLLIE_9P_OPEN")
if (line == "OLLIE_9P_OPEN") {
m_openMarkerSeen = true;
setState(Connected);
}
}
}
@ -87,7 +91,7 @@ void NinePConnection::onFinished(int exitCode, QProcess::ExitStatus)
if (!m_process) return;
m_process->deleteLater();
m_process = nullptr;
if (m_probe && m_state == Connected && exitCode == 0 && !m_stopRequested) {
if (m_probe && m_state == Connected && m_openMarkerSeen && exitCode == 0 && !m_stopRequested) {
m_retryTimer.start(m_retryMs);
return;
}

View File

@ -46,6 +46,7 @@ private:
bool m_stopRequested = false;
int m_retryMs = 4000;
bool m_probe = false;
bool m_openMarkerSeen = false;
};
#endif // NINEPCONNECTION_H

View File

@ -46,11 +46,18 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
emit chatReceived(QString::fromUtf8(data));
});
connect(m_chat, &StreamFsm::died, this, [this]() {
// The chat stream closes when the selected session ends. Treat that
// existing signal as an immediate disconnect for connection tracking;
// the periodic connection probe will confirm or recover it later.
if (!m_activeSessionId.isEmpty() && !m_agentId.isEmpty())
setAgentConnected(agentKey(m_activeSessionId, m_agentId), false);
if (!m_activeSessionId.isEmpty()) {
for (const QVariant &value : std::as_const(m_sessions)) {
const QVariantMap session = value.toMap();
if (session.value("id").toString() != m_activeSessionId) continue;
for (const QVariant &agentValue : session.value("agents").toList()) {
const QString key = agentKey(m_activeSessionId,
agentValue.toMap().value("id").toString());
setAgentConnected(key, false);
}
break;
}
}
emit streamingDone();
});
@ -204,22 +211,27 @@ QString Ollie9pClient::sessionConnectionColor(const QString &sessionId) const
void Ollie9pClient::setAgentConnected(const QString &key, bool connected)
{
if (m_agentConnected.value(key, false) == connected) return;
const bool cached = m_agentConnected.value(key, false);
m_agentConnected[key] = connected;
bool modelChanged = false;
// Update the in-memory model without starting another round of probes.
for (QVariant &value : m_sessions) {
QVariantMap session = value.toMap();
QVariantList agents = session.value("agents").toList();
for (QVariant &agentValue : agents) {
QVariantMap agent = agentValue.toMap();
if (agentKey(session["id"].toString(), agent["id"].toString()) == key)
if (agentKey(session["id"].toString(), agent["id"].toString()) == key) {
if (agent.value("connected").toBool() != connected)
modelChanged = true;
agent["connected"] = connected;
}
agentValue = agent;
}
session["agents"] = agents;
value = session;
}
emit sessionsChanged();
if (cached != connected || modelChanged)
emit sessionsChanged();
}
void Ollie9pClient::startAgentConnections()
@ -244,7 +256,14 @@ void Ollie9pClient::reconcileAgentConnections()
auto *connection = new NinePConnection(this);
m_agentConnections.insert(key, connection);
connect(connection, &NinePConnection::connected, this, [this, key]() {
const bool reconnected = m_seenAgentConnections.contains(key);
m_seenAgentConnections.insert(key);
setAgentConnected(key, true);
if (key == agentKey(m_activeSessionId, m_agentId)) {
if (reconnected)
emit sessionConnected();
startActiveAgentStreams();
}
});
connect(connection, &NinePConnection::disconnected, this, [this, key]() {
setAgentConnected(key, false);
@ -457,11 +476,7 @@ void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId
emit activeAgentIdChanged();
emit activeStateChanged();
// switchAgent is the ONLY path that starts per-agent streams.
QString srv = serverAddr();
QString bin = ninepBin();
m_chat->start(bin, {"-a", srv, "read", agentPath() + "/chat"});
m_state->start(bin, {"-a", srv, "read", agentPath() + "/statewait"});
startActiveAgentStreams();
}
void Ollie9pClient::loadRootBackends()
@ -691,6 +706,16 @@ void Ollie9pClient::ensureRootDataLoaded()
// --- Streaming ---
void Ollie9pClient::startActiveAgentStreams()
{
if (m_activeSessionId.isEmpty() || m_agentId.isEmpty()) return;
const QString srv = serverAddr();
const QString bin = ninepBin();
m_chat->start(bin, {"-a", srv, "read", agentPath() + "/chat"});
m_state->start(bin, {"-a", srv, "read", agentPath() + "/statewait"});
}
void Ollie9pClient::stopAgentStreams()
{
if (m_chat) m_chat->stop();

View File

@ -4,6 +4,7 @@
#include <QObject>
#include <QVariantList>
#include <QHash>
#include <QSet>
#if QT_VERSION_MAJOR >= 6
#include <QQmlEngine>
#endif
@ -93,6 +94,7 @@ signals:
void activeAgentIdChanged();
void activeStateChanged();
void chatReceived(const QString &text);
void sessionConnected();
void streamingDone(); // chat process exited (session killed)
void rootBackendsLoadedChanged();
void availableBackendsChanged();
@ -112,6 +114,7 @@ private:
void ensureRootDataLoaded();
void setDaemonConnected(bool connected);
void startAgentConnections();
void startActiveAgentStreams();
void reconcileAgentConnections();
void stopAgentConnections();
@ -131,6 +134,7 @@ private:
bool m_daemonConnected = false;
QHash<QString, NinePConnection *> m_agentConnections;
QHash<QString, bool> m_agentConnected;
QSet<QString> m_seenAgentConnections;
NinePConnection *m_daemon = nullptr;
// Streaming FSMs — each wraps a QProcess lifecycle