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:
teernisse
2026-03-05 13:01:55 -05:00
parent 571c304031
commit ffbd1e2dce
8 changed files with 795 additions and 19 deletions

View File

@@ -1,8 +1,8 @@
use crate::cli::render::{self, Align, GlyphMode, Icons, LoreRenderer, StyledCell, Table, Theme};
use super::types::{
ActivityEventType, AttentionState, MeActivityEvent, MeDashboard, MeIssue, MeMr, MeSummary,
SinceLastCheck,
ActivityEventType, AttentionState, MeActivityEvent, MeDashboard, MeIssue, MeMention, MeMr,
MeSummary, SinceLastCheck,
};
// ─── Layout Helpers ─────────────────────────────────────────────────────────
@@ -164,12 +164,19 @@ pub fn print_summary_header(summary: &MeSummary, username: &str) {
Theme::dim().render("0 need attention")
};
let mentioned = if summary.mentioned_in_count > 0 {
format!(" {} mentioned", summary.mentioned_in_count)
} else {
String::new()
};
println!(
" {} projects {} issues {} authored MRs {} reviewing MRs {}",
" {} projects {} issues {} authored MRs {} reviewing MRs{} {}",
summary.project_count,
summary.open_issue_count,
summary.authored_mr_count,
summary.reviewing_mr_count,
mentioned,
needs,
);
@@ -342,6 +349,53 @@ pub fn print_reviewing_mrs_section(mrs: &[MeMr], single_project: bool) {
}
}
// ─── Mentioned In Section ────────────────────────────────────────────────
/// Print the "Mentioned In" section for items where user is @mentioned but
/// not assigned, authored, or reviewing.
pub fn print_mentioned_in_section(mentions: &[MeMention], single_project: bool) {
if mentions.is_empty() {
return;
}
println!(
"{}",
render::section_divider(&format!("Mentioned In ({})", mentions.len()))
);
for item in mentions {
let attn = styled_attention(&item.attention_state);
let ref_str = match item.entity_type.as_str() {
"issue" => format!("#{}", item.iid),
"mr" => format!("!{}", item.iid),
_ => format!("{}:{}", item.entity_type, item.iid),
};
let ref_style = match item.entity_type.as_str() {
"issue" => Theme::issue_ref(),
"mr" => Theme::mr_ref(),
_ => Theme::bold(),
};
let state_tag = match item.state.as_str() {
"opened" => String::new(),
other => format!(" [{}]", other),
};
let time = render::format_relative_time(item.updated_at);
println!(
" {} {} {}{} {}",
attn,
ref_style.render(&ref_str),
render::truncate(&item.title, title_width(43)),
Theme::dim().render(&state_tag),
Theme::dim().render(&time),
);
if !single_project {
println!(" {}", Theme::dim().render(&item.project_path));
}
}
}
// ─── Activity Feed ───────────────────────────────────────────────────────────
/// Print the activity feed section (Task #17).
@@ -587,6 +641,7 @@ pub fn print_me_dashboard(dashboard: &MeDashboard, single_project: bool) {
print_issues_section(&dashboard.open_issues, single_project);
print_authored_mrs_section(&dashboard.open_mrs_authored, single_project);
print_reviewing_mrs_section(&dashboard.reviewing_mrs, single_project);
print_mentioned_in_section(&dashboard.mentioned_in, single_project);
print_activity_section(&dashboard.activity, single_project);
println!();
}
@@ -597,6 +652,7 @@ pub fn print_me_dashboard_filtered(
single_project: bool,
show_issues: bool,
show_mrs: bool,
show_mentions: bool,
show_activity: bool,
) {
if let Some(ref since) = dashboard.since_last_check {
@@ -611,6 +667,9 @@ pub fn print_me_dashboard_filtered(
print_authored_mrs_section(&dashboard.open_mrs_authored, single_project);
print_reviewing_mrs_section(&dashboard.reviewing_mrs, single_project);
}
if show_mentions {
print_mentioned_in_section(&dashboard.mentioned_in, single_project);
}
if show_activity {
print_activity_section(&dashboard.activity, single_project);
}