From 4c53898382158467562334f6536ca7c0d537db42 Mon Sep 17 00:00:00 2001 From: damocles Date: Tue, 23 Jun 2026 15:11:28 +0200 Subject: [PATCH] fix(#1940): carry pr_number on the live approval_added event --- hive-c0re/src/coordinator.rs | 34 ++++++++++++++------ hive-c0re/src/dashboard.rs | 10 +++++- hive-c0re/src/dashboard_events.rs | 8 +++++ hive-c0re/src/socket_server.rs | 53 +++++++++++++++++++------------ 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 9fec0107..a4591049 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -389,6 +389,20 @@ pub struct ApprovalResolved<'a> { pub description: Option, } +/// Field-named payload for [`Coordinator::emit_approval_added`]. +/// Mirrors the `ApprovalAdded` dashboard-event fields. `agent` +/// borrows from the caller; `approval_kind` is a compile-time +/// constant. `pr_number` is set for `merge_config_pr` only. +pub struct ApprovalAdded<'a> { + pub id: i64, + pub agent: &'a str, + pub approval_kind: &'static str, + pub sha_short: Option, + pub diff: Option, + pub description: Option, + pub pr_number: Option, +} + /// Field-named payload for [`Coordinator::emit_question_added`]. /// Mirrors the `QuestionAdded` dashboard-event fields; all references /// share the caller's lifetime. @@ -740,15 +754,16 @@ impl Coordinator { /// Emit `ApprovalAdded` immediately after the row is inserted in /// sqlite. Caller passes the diff text it already computed (or /// `None` for spawn approvals which carry no diff). - pub fn emit_approval_added( - &self, - id: i64, - agent: &str, - approval_kind: &'static str, - sha_short: Option, - diff: Option, - description: Option, - ) { + pub fn emit_approval_added(&self, ev: ApprovalAdded<'_>) { + let ApprovalAdded { + id, + agent, + approval_kind, + sha_short, + diff, + description, + pr_number, + } = ev; self.emit_dashboard_event(DashboardEvent::ApprovalAdded { seq: self.next_seq(), id, @@ -757,6 +772,7 @@ impl Coordinator { sha_short, diff, description, + pr_number, }); } diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index a321b31e..9b39128b 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -1663,7 +1663,15 @@ async fn post_request_spawn( // refetch. Spawn approvals carry no diff/sha. state .coord - .emit_approval_added(id, &name, "spawn", None, None, None); + .emit_approval_added(crate::coordinator::ApprovalAdded { + id, + agent: &name, + approval_kind: "spawn", + sha_short: None, + diff: None, + description: None, + pr_number: None, + }); (StatusCode::OK, "ok").into_response() } Err(e) => error_response(&format!("request-spawn {name} failed: {e:#}")), diff --git a/hive-c0re/src/dashboard_events.rs b/hive-c0re/src/dashboard_events.rs index 7cf02437..13307bcb 100644 --- a/hive-c0re/src/dashboard_events.rs +++ b/hive-c0re/src/dashboard_events.rs @@ -76,6 +76,13 @@ pub enum DashboardEvent { sha_short: Option, diff: Option, description: Option, + /// Forge PR number, for `merge_config_pr` approvals only — lets + /// the live `applyApprovalAdded` path build the "review PR on + /// forge" link without waiting for a cold `/api/state` refresh + /// (mirrors `ApprovalView::pr_number`). `None` for every other + /// kind. + #[serde(skip_serializing_if = "Option::is_none")] + pr_number: Option, }, /// A pending approval transitioned to a terminal state /// (approved / denied / failed). Clients move the row out of the @@ -346,6 +353,7 @@ mod tests { sha_short: None, diff: None, description: None, + pr_number: None, }, DashboardEvent::ApprovalResolved { seq: 1, diff --git a/hive-c0re/src/socket_server.rs b/hive-c0re/src/socket_server.rs index d7722f4e..f26abe62 100644 --- a/hive-c0re/src/socket_server.rs +++ b/hive-c0re/src/socket_server.rs @@ -1438,14 +1438,15 @@ fn handle_request_update_meta_inputs( } }; tracing::info!(%id, %label, "update_meta_inputs approval queued"); - coord.emit_approval_added( + coord.emit_approval_added(crate::coordinator::ApprovalAdded { id, - requester, - "update_meta_inputs", - None, - None, - description.map(str::to_owned), - ); + agent: requester, + approval_kind: "update_meta_inputs", + sha_short: None, + diff: None, + description: description.map(str::to_owned), + pr_number: None, + }); AgentResponse::Ok } @@ -1542,14 +1543,15 @@ fn handle_request_schedule_prompt( interval = ?payload.interval_seconds, "schedule_prompt approval queued" ); - coord.emit_approval_added( + coord.emit_approval_added(crate::coordinator::ApprovalAdded { id, - requester, - "schedule_prompt", - None, - None, - payload.description.clone(), - ); + agent: requester, + approval_kind: "schedule_prompt", + sha_short: None, + diff: None, + description: payload.description.clone(), + pr_number: None, + }); AgentResponse::Ok } @@ -1802,7 +1804,15 @@ pub(crate) fn submit_init_config( ) .map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?; tracing::info!(%id, %name, "init_config approval queued"); - coord.emit_approval_added(id, name, "init_config", None, None, description); + coord.emit_approval_added(crate::coordinator::ApprovalAdded { + id, + agent: name, + approval_kind: "init_config", + sha_short: None, + diff: None, + description, + pr_number: None, + }); Ok(id) } @@ -1928,14 +1938,15 @@ pub(crate) async fn submit_apply_commit( // get a fully-formed row without a snapshot refetch. `sha_short` // is reused from the dedup gate above. let diff = crate::dashboard::approval_diff(agent, id).await; - coord.emit_approval_added( + coord.emit_approval_added(crate::coordinator::ApprovalAdded { id, agent, - "apply_commit", - Some(sha_short), - Some(diff), - description.map(str::to_owned), - ); + approval_kind: "apply_commit", + sha_short: Some(sha_short), + diff: Some(diff), + description: description.map(str::to_owned), + pr_number: None, + }); Ok((id, sha)) }