agent ui: event-driven status / model / token_usage / turn_state
new LiveEvent variants on the per-agent bus —
status_changed / model_changed / token_usage_changed /
turn_state_changed — replace the per-agent web UI's
/api/state polling for the badge row.
emit sites:
- Bus::set_model → model_changed
- Bus::record_usage → token_usage_changed
- Bus::set_state → turn_state_changed
- turn::wait_for_login → status_changed("online") on creds detect
- post_login_start / post_login_cancel → status_changed("needs_login_*")
per-agent endpoints (post_set_model / post_compact / post_new_session
/ post_cancel_turn / post_login_*) now all return 200; client
drops the post-submit refetch except on login transitions, which
still need /api/state to render the OAuth form + session stream.
client adds dispatch on the four new event kinds, threads
`currentLabel` through so the composer re-enables on a live
status flip, and no longer fires refreshState() from turn_end or
postModel — the events carry the same signal faster.
closes the per-agent half of the dashboard event-channel
refactor; TODO entry dropped.
This commit is contained in:
parent
b444dac6e8
commit
39d8359c10
7 changed files with 120 additions and 22 deletions
|
|
@ -118,6 +118,30 @@ pub enum LiveEvent {
|
|||
/// Turn finished. `ok=false` means claude exited non-zero or the
|
||||
/// harness hit a transport error.
|
||||
TurnEnd { ok: bool, note: Option<String> },
|
||||
/// Harness reachability flipped: `"online"` /
|
||||
/// `"needs_login_idle"` / `"needs_login_in_progress"`. The web UI
|
||||
/// drives the alive badge from this so the operator sees a login
|
||||
/// land (or get revoked) without polling. Session detail
|
||||
/// (`url`/`output`/`finished`) is still served by `/api/state`
|
||||
/// during the short-lived in-progress window — the client
|
||||
/// re-fetches only while that flow is active.
|
||||
StatusChanged { status: String },
|
||||
/// `/api/model` switched the active claude model. The web UI
|
||||
/// updates the chip + the per-turn stats sink will key off this
|
||||
/// to mark the boundary in its log.
|
||||
ModelChanged { model: String },
|
||||
/// Final-turn `usage` block landed (input + output + cache
|
||||
/// counters). Powers the context-window badge + accumulates into
|
||||
/// the per-turn stats sink.
|
||||
TokenUsageChanged { usage: TokenUsage },
|
||||
/// Harness's `TurnState` transitioned (idle / thinking /
|
||||
/// compacting). `since_unix` matches `Bus::state_snapshot().1`
|
||||
/// so the client's elapsed-time ticker keeps progressing across
|
||||
/// SSE reconnects without drift.
|
||||
TurnStateChanged {
|
||||
state: TurnState,
|
||||
since_unix: i64,
|
||||
},
|
||||
}
|
||||
|
||||
/// sqlite-backed event log. Wraps a `Connection` behind a `Mutex` so the
|
||||
|
|
@ -149,6 +173,10 @@ impl EventStore {
|
|||
LiveEvent::Stream(_) => "stream",
|
||||
LiveEvent::Note { .. } => "note",
|
||||
LiveEvent::TurnEnd { .. } => "turn_end",
|
||||
LiveEvent::StatusChanged { .. } => "status_changed",
|
||||
LiveEvent::ModelChanged { .. } => "model_changed",
|
||||
LiveEvent::TokenUsageChanged { .. } => "token_usage_changed",
|
||||
LiveEvent::TurnStateChanged { .. } => "turn_state_changed",
|
||||
};
|
||||
let payload = serde_json::to_string(event).unwrap_or_else(|_| "null".into());
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
|
@ -216,7 +244,7 @@ impl TokenUsage {
|
|||
/// reads via `/api/state` and renders. Lives alongside the bus
|
||||
/// because everyone who has a `Bus` already has the right handle to
|
||||
/// poke the state on transitions.
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TurnState {
|
||||
/// Inbox is empty / waiting on `Recv`.
|
||||
|
|
@ -340,11 +368,13 @@ impl Bus {
|
|||
if let Err(e) = persist_model(&value) {
|
||||
tracing::warn!(error = ?e, "model: persist failed");
|
||||
}
|
||||
self.emit(LiveEvent::ModelChanged { model: value });
|
||||
}
|
||||
|
||||
/// Record the latest token usage from a completed turn.
|
||||
pub fn record_usage(&self, usage: TokenUsage) {
|
||||
*self.last_usage.lock().unwrap() = Some(usage);
|
||||
self.emit(LiveEvent::TokenUsageChanged { usage });
|
||||
}
|
||||
|
||||
/// Last known token usage, or `None` if no turn has completed yet.
|
||||
|
|
@ -356,11 +386,31 @@ impl Bus {
|
|||
/// Update the harness's authoritative turn-loop state. Records
|
||||
/// the transition time so `state_snapshot` can return a since-age.
|
||||
pub fn set_state(&self, next: TurnState) {
|
||||
let mut guard = self.state.lock().unwrap();
|
||||
if guard.0 == next {
|
||||
return;
|
||||
let since;
|
||||
{
|
||||
let mut guard = self.state.lock().unwrap();
|
||||
if guard.0 == next {
|
||||
return;
|
||||
}
|
||||
*guard = (next, now_unix());
|
||||
since = guard.1;
|
||||
}
|
||||
*guard = (next, now_unix());
|
||||
self.emit(LiveEvent::TurnStateChanged {
|
||||
state: next,
|
||||
since_unix: since,
|
||||
});
|
||||
}
|
||||
|
||||
/// Broadcast a status flip (online / needs_login_*). Called by
|
||||
/// the bin entry points + `turn::wait_for_login` + the
|
||||
/// `post_login_*` handlers — every site that mutates the
|
||||
/// `Arc<Mutex<LoginState>>` should also call this so the web UI
|
||||
/// drops its periodic /api/state poll while a turn loop is
|
||||
/// running.
|
||||
pub fn emit_status(&self, status: impl Into<String>) {
|
||||
self.emit(LiveEvent::StatusChanged {
|
||||
status: status.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Current state + since-when (unix seconds). Snapshot copy, no lock held.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue