live agent state indicators in session tree
- Add per-agent statewait streams to track execution state - Agent dots show: green=idle, orange=calling, blue=thinking - Session dots show RPC connection health (unchanged) - Use version counter pattern to force QML binding re-evaluation
This commit is contained in:
parent
6210fccf30
commit
453cef397f
28
gui/main.qml
28
gui/main.qml
|
|
@ -11,6 +11,9 @@ ApplicationWindow {
|
|||
font.family: theme.fontFamily || Qt.application.font.family
|
||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
||||
|
||||
// Version counter to force re-evaluation of agent state colors
|
||||
property int agentStateVersion: 0
|
||||
|
||||
// Apply theme palette to all Qt Quick Controls
|
||||
palette.active.window: theme.windowColor
|
||||
palette.active.windowText: theme.windowTextColor
|
||||
|
|
@ -130,12 +133,14 @@ ApplicationWindow {
|
|||
})
|
||||
for (var j = 0; j < agents.length; ++j) {
|
||||
var agent = agents[j]
|
||||
// Use live state if available, otherwise fall back to refresh-time state
|
||||
var liveState = ollie.agentState(sessionId, agent.id)
|
||||
sessionTreeModel.append({
|
||||
type: "agent",
|
||||
parentId: sessionId,
|
||||
agentId: agent.id,
|
||||
level: 1,
|
||||
state: "idle",
|
||||
state: liveState || agent.state || "idle",
|
||||
connected: agent.connected,
|
||||
displayName: agent.name
|
||||
})
|
||||
|
|
@ -205,6 +210,10 @@ ApplicationWindow {
|
|||
function onSessionsChanged() {
|
||||
buildSessionTree()
|
||||
}
|
||||
function onAgentStateChanged(sessionId, agentId, state) {
|
||||
// Increment version to force re-evaluation of all agent state colors
|
||||
root.agentStateVersion++
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
|
|
@ -779,20 +788,25 @@ ApplicationWindow {
|
|||
|
||||
// State dot
|
||||
Rectangle {
|
||||
id: stateDot
|
||||
Layout.preferredWidth: 8
|
||||
Layout.preferredHeight: 8
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: 4
|
||||
property int stateVersion: root.agentStateVersion
|
||||
color: {
|
||||
var s = state || "idle"
|
||||
if (type === "agent")
|
||||
return connected ? "#4caf50" : "#f44336"
|
||||
// Depend on stateVersion to force re-evaluation
|
||||
void(stateVersion)
|
||||
if (type === "session") {
|
||||
if (connectionColor === "green") return "#4caf50"
|
||||
if (connectionColor === "yellow") return "#ffca28"
|
||||
if (connectionColor === "red") return "#f44336"
|
||||
// Session: RPC connection health
|
||||
var cc = connectionColor || "gray"
|
||||
if (cc === "green") return "#4caf50"
|
||||
if (cc === "yellow") return "#ffca28"
|
||||
if (cc === "red") return "#f44336"
|
||||
return "#9e9e9e"
|
||||
}
|
||||
// Agent: execution state - query live state
|
||||
var s = ollie.agentState(parentId, agentId) || "idle"
|
||||
if (s === "idle") return "#4caf50"
|
||||
if (s.startsWith("calling")) return "#ff9800"
|
||||
return "#2196f3"
|
||||
|
|
|
|||
|
|
@ -84,10 +84,12 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
|
|||
refreshSessions();
|
||||
ensureRootDataLoaded();
|
||||
startAgentConnections();
|
||||
reconcileAgentStateStreams();
|
||||
});
|
||||
connect(m_daemon, &NinePConnection::disconnected, this, [this]() {
|
||||
setDaemonConnected(false);
|
||||
stopAgentConnections();
|
||||
stopAgentStateStreams();
|
||||
});
|
||||
connect(m_daemon, &NinePConnection::readyRead, this, [this](const QByteArray &) {
|
||||
refreshSessions();
|
||||
|
|
@ -286,6 +288,70 @@ void Ollie9pClient::stopAgentConnections()
|
|||
m_agentConnections.clear();
|
||||
}
|
||||
|
||||
void Ollie9pClient::reconcileAgentStateStreams()
|
||||
{
|
||||
if (!m_daemonConnected) return;
|
||||
QSet<QString> wanted;
|
||||
for (const QVariant &value : std::as_const(m_sessions)) {
|
||||
const QVariantMap session = value.toMap();
|
||||
const QString sid = session.value("id").toString();
|
||||
const bool paused = session.value("paused").toBool();
|
||||
for (const QVariant &agentValue : session.value("agents").toList()) {
|
||||
const QVariantMap agent = agentValue.toMap();
|
||||
const QString aid = agent.value("id").toString();
|
||||
const QString key = agentKey(sid, aid);
|
||||
wanted.insert(key);
|
||||
if (m_agentStateStreams.contains(key)) continue;
|
||||
if (paused) continue; // Don't start streams for paused sessions
|
||||
auto *stream = new StreamFsm(StreamFsm::Looping, this);
|
||||
m_agentStateStreams.insert(key, stream);
|
||||
m_agentStateValues.insert(key, "idle");
|
||||
connect(stream, &StreamFsm::readyRead, this, [this, key, sid, aid](const QByteArray &data) {
|
||||
QString state = QString::fromUtf8(data).trimmed();
|
||||
if (!state.isEmpty() && m_agentStateValues.value(key) != state) {
|
||||
m_agentStateValues[key] = state;
|
||||
emit agentStateChanged(sid, aid, state);
|
||||
// Also update activeState if this is the active agent
|
||||
if (sid == m_activeSessionId && aid == m_agentId) {
|
||||
m_activeState = state;
|
||||
emit activeStateChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
stream->start(ollie9pBin(), {"-a", serverAddr(), "read",
|
||||
"session/" + sid + "/agent/" + aid + "/statewait"});
|
||||
}
|
||||
}
|
||||
// Remove streams for agents that no longer exist
|
||||
for (auto it = m_agentStateStreams.begin(); it != m_agentStateStreams.end();) {
|
||||
if (wanted.contains(it.key())) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
it.value()->disconnect(this);
|
||||
it.value()->stop();
|
||||
it.value()->deleteLater();
|
||||
m_agentStateValues.remove(it.key());
|
||||
it = m_agentStateStreams.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Ollie9pClient::stopAgentStateStreams()
|
||||
{
|
||||
for (StreamFsm *stream : std::as_const(m_agentStateStreams)) {
|
||||
stream->disconnect(this);
|
||||
stream->stop();
|
||||
}
|
||||
qDeleteAll(m_agentStateStreams);
|
||||
m_agentStateStreams.clear();
|
||||
m_agentStateValues.clear();
|
||||
}
|
||||
|
||||
QString Ollie9pClient::agentState(const QString &sessionId, const QString &agentId) const
|
||||
{
|
||||
return m_agentStateValues.value(agentKey(sessionId, agentId), "idle");
|
||||
}
|
||||
|
||||
void Ollie9pClient::refreshSessions()
|
||||
{
|
||||
if (!m_daemonConnected) return;
|
||||
|
|
@ -321,22 +387,26 @@ void Ollie9pClient::refreshSessions()
|
|||
QString connectedStr = QString::fromUtf8(run9p({"read", "session/" + sessionId + "/connected"})).trimmed();
|
||||
session["connected"] = (connectedStr == "true");
|
||||
QVariantList agents;
|
||||
for (const QString &name : QString::fromUtf8(run9p({"ls", "session/" + sessionId + "/agent"})).trimmed().split('\n', Qt::SkipEmptyParts)) {
|
||||
if (name == "new") continue;
|
||||
const QString path = "session/" + sessionId + "/agent/" + name;
|
||||
AgentRecord a;
|
||||
a.id = QString::fromUtf8(run9p({"read", path + "/id"})).trimmed();
|
||||
a.name = name;
|
||||
if (a.id.isEmpty()) a.id = name;
|
||||
QVariantMap av; av["id"] = a.id; av["name"] = a.name;
|
||||
av["connected"] = m_daemonConnected && m_agentConnected.value(agentKey(sessionId, a.id), false);
|
||||
agents.append(av);
|
||||
}
|
||||
session["agents"] = agents;
|
||||
for (const QString &name : QString::fromUtf8(run9p({"ls", "session/" + sessionId + "/agent"})).trimmed().split('\n', Qt::SkipEmptyParts)) {
|
||||
if (name == "new") continue;
|
||||
const QString path = "session/" + sessionId + "/agent/" + name;
|
||||
AgentRecord a;
|
||||
a.id = QString::fromUtf8(run9p({"read", path + "/id"})).trimmed();
|
||||
a.name = name;
|
||||
if (a.id.isEmpty()) a.id = name;
|
||||
QString agentState = QString::fromUtf8(run9p({"read", path + "/state"})).trimmed();
|
||||
if (agentState.isEmpty()) agentState = "idle";
|
||||
QVariantMap av; av["id"] = a.id; av["name"] = a.name;
|
||||
av["state"] = agentState;
|
||||
av["connected"] = m_daemonConnected && m_agentConnected.value(agentKey(sessionId, a.id), false);
|
||||
agents.append(av);
|
||||
}
|
||||
session["agents"] = agents;
|
||||
m_sessions.append(session);
|
||||
}
|
||||
}
|
||||
reconcileAgentConnections();
|
||||
reconcileAgentStateStreams();
|
||||
emit sessionsChanged();
|
||||
|
||||
// Check if the currently active session/agent still exist
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public:
|
|||
QString currentBackend() const { return m_currentBackend; }
|
||||
bool daemonConnected() const { return m_daemonConnected; }
|
||||
Q_INVOKABLE bool agentConnected(const QString &sessionId, const QString &agentId) const;
|
||||
Q_INVOKABLE QString agentState(const QString &sessionId, const QString &agentId) const;
|
||||
Q_INVOKABLE QString sessionConnectionColor(const QString &sessionId) const;
|
||||
Q_INVOKABLE void setCurrentBackend(const QString &backend) {
|
||||
if (m_currentBackend == backend) return;
|
||||
|
|
@ -94,6 +95,7 @@ signals:
|
|||
void activeSessionIdChanged();
|
||||
void activeAgentIdChanged();
|
||||
void activeStateChanged();
|
||||
void agentStateChanged(const QString &sessionId, const QString &agentId, const QString &state);
|
||||
void chatReceived(const QString &text);
|
||||
void sessionConnected();
|
||||
void streamingDone(); // chat process exited (session killed)
|
||||
|
|
@ -115,7 +117,9 @@ private:
|
|||
void startAgentConnections();
|
||||
void startActiveAgentStreams();
|
||||
void reconcileAgentConnections();
|
||||
void reconcileAgentStateStreams();
|
||||
void stopAgentConnections();
|
||||
void stopAgentStateStreams();
|
||||
|
||||
QVariantList m_sessions;
|
||||
QString m_activeSessionId;
|
||||
|
|
@ -136,6 +140,10 @@ private:
|
|||
QSet<QString> m_seenAgentConnections;
|
||||
NinePConnection *m_daemon = nullptr;
|
||||
|
||||
// Per-agent state tracking
|
||||
QHash<QString, StreamFsm *> m_agentStateStreams;
|
||||
QHash<QString, QString> m_agentStateValues;
|
||||
|
||||
// Streaming FSMs — each wraps a QProcess lifecycle
|
||||
StreamFsm *m_chat = nullptr; // Oneshot — reads agent chat log
|
||||
StreamFsm *m_state = nullptr; // Guarded — reads agent statewait
|
||||
|
|
|
|||
Reference in New Issue