feat(me): add mentions section for @-mentions in dashboard
Add a new --mentions flag to the `lore me` command that surfaces items where the user is @-mentioned but NOT already assigned, authoring, or reviewing. This fills an important gap in the personal work dashboard: cross-team requests and callouts that don't show up in the standard issue/MR sections. Implementation details: - query_mentioned_in() scans notes for @username patterns, then filters out entities where the user is already an assignee, author, or reviewer - MentionedInItem type captures entity_type (issue/mr), iid, title, state, project path, attention state, and updated timestamp - Attention state computation marks items as needs_attention when there's recent activity from others - Recency cutoff (7 days) prevents surfacing stale mentions - Both human and robot renderers include the new section The robot mode schema adds mentioned_in array with me_mentions field preset for token-efficient output. Test coverage: - mentioned_in_finds_mention_on_unassigned_issue: basic case - mentioned_in_excludes_assigned_issue: no duplicate surfacing - mentioned_in_excludes_author_on_mr: author already sees in authored MRs - mentioned_in_excludes_reviewer_on_mr: reviewer already sees in reviewing - mentioned_in_uses_recency_cutoff: old mentions filtered - mentioned_in_respects_project_filter: scoping works Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,8 @@ use crate::cli::robot::RobotMeta;
|
||||
use crate::core::time::ms_to_iso;
|
||||
|
||||
use super::types::{
|
||||
ActivityEventType, AttentionState, MeActivityEvent, MeDashboard, MeIssue, MeMr, MeSummary,
|
||||
SinceCheckEvent, SinceCheckGroup, SinceLastCheck,
|
||||
ActivityEventType, AttentionState, MeActivityEvent, MeDashboard, MeIssue, MeMention, MeMr,
|
||||
MeSummary, SinceCheckEvent, SinceCheckGroup, SinceLastCheck,
|
||||
};
|
||||
|
||||
// ─── Robot JSON Output (Task #18) ────────────────────────────────────────────
|
||||
@@ -28,11 +28,15 @@ pub fn print_me_json(
|
||||
// Apply --fields filtering (Task #19)
|
||||
if let Some(f) = fields {
|
||||
let expanded = crate::cli::robot::expand_fields_preset(f, "me_items");
|
||||
// Filter all item arrays
|
||||
// Filter issue/MR arrays with the items preset
|
||||
for key in &["open_issues", "open_mrs_authored", "reviewing_mrs"] {
|
||||
crate::cli::robot::filter_fields(&mut value, key, &expanded);
|
||||
}
|
||||
|
||||
// Mentioned-in gets its own preset (needs entity_type + state to disambiguate)
|
||||
let mentions_expanded = crate::cli::robot::expand_fields_preset(f, "me_mentions");
|
||||
crate::cli::robot::filter_fields(&mut value, "mentioned_in", &mentions_expanded);
|
||||
|
||||
// Activity gets its own minimal preset
|
||||
let activity_expanded = crate::cli::robot::expand_fields_preset(f, "me_activity");
|
||||
crate::cli::robot::filter_fields(&mut value, "activity", &activity_expanded);
|
||||
@@ -84,6 +88,7 @@ struct MeDataJson {
|
||||
open_issues: Vec<IssueJson>,
|
||||
open_mrs_authored: Vec<MrJson>,
|
||||
reviewing_mrs: Vec<MrJson>,
|
||||
mentioned_in: Vec<MentionJson>,
|
||||
activity: Vec<ActivityJson>,
|
||||
}
|
||||
|
||||
@@ -97,6 +102,7 @@ impl MeDataJson {
|
||||
open_issues: d.open_issues.iter().map(IssueJson::from).collect(),
|
||||
open_mrs_authored: d.open_mrs_authored.iter().map(MrJson::from).collect(),
|
||||
reviewing_mrs: d.reviewing_mrs.iter().map(MrJson::from).collect(),
|
||||
mentioned_in: d.mentioned_in.iter().map(MentionJson::from).collect(),
|
||||
activity: d.activity.iter().map(ActivityJson::from).collect(),
|
||||
}
|
||||
}
|
||||
@@ -110,6 +116,7 @@ struct SummaryJson {
|
||||
open_issue_count: usize,
|
||||
authored_mr_count: usize,
|
||||
reviewing_mr_count: usize,
|
||||
mentioned_in_count: usize,
|
||||
needs_attention_count: usize,
|
||||
}
|
||||
|
||||
@@ -120,6 +127,7 @@ impl From<&MeSummary> for SummaryJson {
|
||||
open_issue_count: s.open_issue_count,
|
||||
authored_mr_count: s.authored_mr_count,
|
||||
reviewing_mr_count: s.reviewing_mr_count,
|
||||
mentioned_in_count: s.mentioned_in_count,
|
||||
needs_attention_count: s.needs_attention_count,
|
||||
}
|
||||
}
|
||||
@@ -134,6 +142,7 @@ struct IssueJson {
|
||||
title: String,
|
||||
state: String,
|
||||
attention_state: String,
|
||||
attention_reason: String,
|
||||
status_name: Option<String>,
|
||||
labels: Vec<String>,
|
||||
updated_at_iso: String,
|
||||
@@ -148,6 +157,7 @@ impl From<&MeIssue> for IssueJson {
|
||||
title: i.title.clone(),
|
||||
state: "opened".to_string(),
|
||||
attention_state: attention_state_str(&i.attention_state),
|
||||
attention_reason: i.attention_reason.clone(),
|
||||
status_name: i.status_name.clone(),
|
||||
labels: i.labels.clone(),
|
||||
updated_at_iso: ms_to_iso(i.updated_at),
|
||||
@@ -165,6 +175,7 @@ struct MrJson {
|
||||
title: String,
|
||||
state: String,
|
||||
attention_state: String,
|
||||
attention_reason: String,
|
||||
draft: bool,
|
||||
detailed_merge_status: Option<String>,
|
||||
author_username: Option<String>,
|
||||
@@ -181,6 +192,7 @@ impl From<&MeMr> for MrJson {
|
||||
title: m.title.clone(),
|
||||
state: "opened".to_string(),
|
||||
attention_state: attention_state_str(&m.attention_state),
|
||||
attention_reason: m.attention_reason.clone(),
|
||||
draft: m.draft,
|
||||
detailed_merge_status: m.detailed_merge_status.clone(),
|
||||
author_username: m.author_username.clone(),
|
||||
@@ -191,6 +203,37 @@ impl From<&MeMr> for MrJson {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Mention ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MentionJson {
|
||||
entity_type: String,
|
||||
project: String,
|
||||
iid: i64,
|
||||
title: String,
|
||||
state: String,
|
||||
attention_state: String,
|
||||
attention_reason: String,
|
||||
updated_at_iso: String,
|
||||
web_url: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&MeMention> for MentionJson {
|
||||
fn from(m: &MeMention) -> Self {
|
||||
Self {
|
||||
entity_type: m.entity_type.clone(),
|
||||
project: m.project_path.clone(),
|
||||
iid: m.iid,
|
||||
title: m.title.clone(),
|
||||
state: m.state.clone(),
|
||||
attention_state: attention_state_str(&m.attention_state),
|
||||
attention_reason: m.attention_reason.clone(),
|
||||
updated_at_iso: ms_to_iso(m.updated_at),
|
||||
web_url: m.web_url.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Activity ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -365,6 +408,7 @@ mod tests {
|
||||
title: "Fix auth bug".to_string(),
|
||||
project_path: "group/repo".to_string(),
|
||||
attention_state: AttentionState::NeedsAttention,
|
||||
attention_reason: "Others commented recently; you haven't replied".to_string(),
|
||||
status_name: Some("In progress".to_string()),
|
||||
labels: vec!["bug".to_string()],
|
||||
updated_at: 1_700_000_000_000,
|
||||
@@ -373,6 +417,10 @@ mod tests {
|
||||
let json = IssueJson::from(&issue);
|
||||
assert_eq!(json.iid, 42);
|
||||
assert_eq!(json.attention_state, "needs_attention");
|
||||
assert_eq!(
|
||||
json.attention_reason,
|
||||
"Others commented recently; you haven't replied"
|
||||
);
|
||||
assert_eq!(json.state, "opened");
|
||||
assert_eq!(json.status_name, Some("In progress".to_string()));
|
||||
}
|
||||
@@ -384,6 +432,7 @@ mod tests {
|
||||
title: "Add feature".to_string(),
|
||||
project_path: "group/repo".to_string(),
|
||||
attention_state: AttentionState::AwaitingResponse,
|
||||
attention_reason: "You replied moments ago; awaiting others".to_string(),
|
||||
draft: true,
|
||||
detailed_merge_status: Some("mergeable".to_string()),
|
||||
author_username: Some("alice".to_string()),
|
||||
@@ -394,6 +443,10 @@ mod tests {
|
||||
let json = MrJson::from(&mr);
|
||||
assert_eq!(json.iid, 99);
|
||||
assert_eq!(json.attention_state, "awaiting_response");
|
||||
assert_eq!(
|
||||
json.attention_reason,
|
||||
"You replied moments ago; awaiting others"
|
||||
);
|
||||
assert!(json.draft);
|
||||
assert_eq!(json.author_username, Some("alice".to_string()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user