diff --git a/gui/main.qml b/gui/main.qml index 70f6784..c714043 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -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") } diff --git a/gui/ninepconnection.cpp b/gui/ninepconnection.cpp index 7088f44..9140ea3 100644 --- a/gui/ninepconnection.cpp +++ b/gui/ninepconnection.cpp @@ -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; } diff --git a/gui/ninepconnection.h b/gui/ninepconnection.h index 141834a..18df1f7 100644 --- a/gui/ninepconnection.h +++ b/gui/ninepconnection.h @@ -46,6 +46,7 @@ private: bool m_stopRequested = false; int m_retryMs = 4000; bool m_probe = false; + bool m_openMarkerSeen = false; }; #endif // NINEPCONNECTION_H diff --git a/gui/ollie9pclient.cpp b/gui/ollie9pclient.cpp index a43819a..23bf211 100644 --- a/gui/ollie9pclient.cpp +++ b/gui/ollie9pclient.cpp @@ -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(); diff --git a/gui/ollie9pclient.h b/gui/ollie9pclient.h index 48e4f5b..452399a 100644 --- a/gui/ollie9pclient.h +++ b/gui/ollie9pclient.h @@ -4,6 +4,7 @@ #include #include #include +#include #if QT_VERSION_MAJOR >= 6 #include #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 m_agentConnections; QHash m_agentConnected; + QSet m_seenAgentConnections; NinePConnection *m_daemon = nullptr; // Streaming FSMs — each wraps a QProcess lifecycle