diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 165164d4..d36e1bcd 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -154,12 +154,6 @@ fn truncate(s: &str, max: usize) -> String { /// Build a human-readable wake message for one Forgejo notification. /// Fetches the subject and (if present) the latest comment to include /// author and body. Falls back gracefully when API calls fail. -/// -/// Format: `[kind #N repo/name] title\nurl: \n\nauthor: body` (comments) -/// `[kind #N repo/name] title\nurl: \nassignee: ` (new items) -/// -/// Number is extracted from `html_url` (last path segment before any `#`). -/// Repo slug (`owner/name`) is always included — agents may watch multiple repos. async fn format_notification( client: &reqwest::Client, token: &str, @@ -170,22 +164,7 @@ async fn format_notification( let html_url = notif["subject"]["html_url"] .as_str() .unwrap_or_else(|| notif["subject"]["url"].as_str().unwrap_or("")); - - // Extract issue/PR number from the html_url. URL ends with /issues/N or - // /pulls/N (possibly followed by #anchor for comments). Best-effort. - let num = html_url - .split('#') - .next() - .and_then(|u| u.rsplit('/').next()) - .and_then(|s| s.parse::().ok()) - .map(|n| format!(" #{n}")) - .unwrap_or_default(); - - // Repo slug for multi-repo disambiguation. Falls back gracefully when absent. - let repo = notif["repository"]["full_name"] - .as_str() - .map(|r| format!(" {r}")) - .unwrap_or_default(); + let repo = notif["repository"]["full_name"].as_str().unwrap_or("?"); // API URLs for fetching content let subject_api_url = notif["subject"]["url"].as_str().unwrap_or(""); @@ -211,9 +190,12 @@ async fn format_notification( .unwrap_or("") .trim(); - let kind = format!("comment on {}{num}{repo}", notif_type_label(notif_type)); - let url = if comment_html_url.is_empty() { html_url } else { comment_html_url }; - let mut out = format!("[{kind}] {title}\nurl: {url}\n\n{author}: {}", truncate(body, BODY_TRUNCATE)); + let kind = format!("comment on {}", notif_type_label(notif_type)); + let mut out = format!( + "[{kind}] {title}\nrepo: {repo}\nurl: {}\n\n{author}: {}\n", + if comment_html_url.is_empty() { html_url } else { comment_html_url }, + truncate(body, BODY_TRUNCATE), + ); if out.ends_with('\n') { out.pop(); } @@ -231,33 +213,29 @@ async fn format_notification( let label = notif_type_label(notif_type); let kind = match notif_state { - "merged" => format!("{label} merged{num}{repo}"), - "closed" => format!("{label} closed{num}{repo}"), - "open" | "" => format!("new {label}{num}{repo}"), - other => format!("{label}{num}{repo}: {other}"), + "merged" => format!("{label} merged"), + "closed" => format!("{label} closed"), + "open" | "" => format!("new {label}"), + other => format!("{label}: {other}"), }; - let mut out = format!("[{kind}] {title}\nurl: {html_url}"); - - // For new (open) items: fetch assignee(s). Skip body — agents can - // run `hive-forge view ` for full content. One extra API call - // only on creation events, not state changes. - let is_new = notif_state == "open" || notif_state.is_empty(); - if is_new { + // Fetch subject only for body/author on new (open) items — not + // worth an extra HTTP round-trip for already-closed/merged ones. + let is_open = notif_state == "open" || notif_state.is_empty(); + let mut out = format!("[{kind}] {title}\nrepo: {repo}\nurl: {html_url}"); + if is_open { let subject = fetch_json(client, subject_api_url, token).await; - let assignees: Vec<&str> = subject + let author = subject .as_ref() - .and_then(|s| s["assignees"].as_array()) - .map(|arr| { - arr.iter() - .filter_map(|a| a["login"].as_str()) - .collect() - }) - .unwrap_or_default(); - if assignees.is_empty() { - out.push_str("\nassignee: unassigned"); - } else { - out.push_str(&format!("\nassignee: {}", assignees.join(", "))); + .and_then(|s| s["user"]["login"].as_str()) + .unwrap_or("?"); + let body = subject + .as_ref() + .and_then(|s| s["body"].as_str()) + .unwrap_or("") + .trim(); + if !body.is_empty() { + out.push_str(&format!("\n\n{author}: {}", truncate(body, BODY_TRUNCATE))); } } out