feat: session busy indicator — daemon tracks mid-turn state (agent_start/settled), busy field on session rows + live broadcast; pulsing pip in sidebar and cards (265 web tests, daemon 95.4%)
This commit is contained in:
@@ -100,6 +100,7 @@ type SessionView struct {
|
||||
Repo *string `json:"repo"`
|
||||
StartedAt int64 `json:"startedAt"`
|
||||
Online bool `json:"online"`
|
||||
Busy bool `json:"busy"`
|
||||
LastEventAt int64 `json:"lastEventAt"`
|
||||
}
|
||||
|
||||
@@ -342,6 +343,10 @@ type Hub struct {
|
||||
agents map[string]*agentConn
|
||||
webs map[*webClient]struct{}
|
||||
|
||||
// busy tracks sessions mid-turn (agent_start without agent_settled),
|
||||
// surfaced on session rows so the UI can show a live activity pulse.
|
||||
busy map[string]bool
|
||||
|
||||
// SpawnStatus lets the API push spawn job snapshots to web clients.
|
||||
SpawnStatus func() []SpawnJob
|
||||
|
||||
@@ -353,6 +358,7 @@ func NewHub(store *Store) *Hub {
|
||||
store: store,
|
||||
agents: make(map[string]*agentConn),
|
||||
webs: make(map[*webClient]struct{}),
|
||||
busy: make(map[string]bool),
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
@@ -392,6 +398,7 @@ func (h *Hub) SessionsView() []SessionView {
|
||||
LastEventAt: row.LastEventAt,
|
||||
}
|
||||
_, v.Online = h.agents[row.ID]
|
||||
v.Busy = h.busy[row.ID]
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
@@ -607,6 +614,18 @@ func (h *Hub) handleEvent(f frame) {
|
||||
log.Printf("hub: touch session %s: %v", f.sessionID, err)
|
||||
}
|
||||
}
|
||||
// Track mid-turn state for the session-list activity pulse.
|
||||
if f.typ == evAgentStart || f.typ == evAgentSettled {
|
||||
busy := f.typ == evAgentStart
|
||||
h.mu.Lock()
|
||||
if h.busy[f.sessionID] != busy {
|
||||
h.busy[f.sessionID] = busy
|
||||
h.mu.Unlock()
|
||||
go h.BroadcastSessionList()
|
||||
} else {
|
||||
h.mu.Unlock()
|
||||
}
|
||||
}
|
||||
h.publishEvent(f.sessionID, f.seq, json.RawMessage(f.raw))
|
||||
}
|
||||
|
||||
@@ -624,6 +643,9 @@ func (h *Hub) unregister(ac *agentConn) {
|
||||
if err := h.store.SetOnline(ac.sessionID, false); err != nil {
|
||||
log.Printf("hub: mark offline %s: %v", ac.sessionID, err)
|
||||
}
|
||||
h.mu.Lock()
|
||||
delete(h.busy, ac.sessionID)
|
||||
h.mu.Unlock()
|
||||
}
|
||||
h.BroadcastSessionList()
|
||||
}
|
||||
|
||||
@@ -714,3 +714,34 @@ func TestWebResubscribeSplitsEventBatches(t *testing.T) {
|
||||
_ = agent2.WriteJSON(map[string]any{"v": 1, "type": evMessageUpdate, "sessionId": "s2", "seq": 1, "ts": 11, "delta": "two"})
|
||||
readBatch("s2") // event B must arrive as its own frame, never mixed with A's
|
||||
}
|
||||
|
||||
func TestHubBusyFlagLifecycle(t *testing.T) {
|
||||
ts, _, hub := newTestServerHub(t)
|
||||
ws := dialAgent(t, ts)
|
||||
defer ws.Close()
|
||||
_ = ws.WriteJSON(helloFrame("busy-1"))
|
||||
_ = readFrame(t, ws)
|
||||
sendEv := func(typ string, seq int64) {
|
||||
_ = ws.WriteJSON(map[string]any{
|
||||
"v": 1, "sessionId": "busy-1", "seq": seq, "ts": 1, "type": typ,
|
||||
})
|
||||
}
|
||||
sendEv("agent_start", 1)
|
||||
waitFor(t, 3*time.Second, func() bool {
|
||||
for _, s := range hub.SessionsView() {
|
||||
if s.ID == "busy-1" {
|
||||
return s.Busy
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
sendEv("agent_settled", 2)
|
||||
waitFor(t, 3*time.Second, func() bool {
|
||||
for _, s := range hub.SessionsView() {
|
||||
if s.ID == "busy-1" {
|
||||
return !s.Busy
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user