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:
Raphael Westphal
2026-08-19 10:41:40 +02:00
parent c8ad68aaea
commit 957ea03aa1
11 changed files with 137 additions and 3 deletions
+22
View File
@@ -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()
}