feat(maestro): render agent-reported detail on TUI session cards
This commit is contained in:
@@ -160,12 +160,26 @@ impl Sidebar {
|
||||
spans.push(Span::styled(" ", base));
|
||||
spans.push(Span::styled(format_state_duration(d), dot_style));
|
||||
}
|
||||
// The trailing fragments are measured up front so a long detail
|
||||
// truncates ahead of them instead of pushing them off the line
|
||||
// (docs/SIDEBAR.md).
|
||||
let mut tail: Vec<Span<'static>> = Vec::new();
|
||||
if let Some(b) = blocked {
|
||||
spans.push(Span::styled(format!(" ⚠ {b}"), muted));
|
||||
tail.push(Span::styled(format!(" ⚠ {b}"), muted));
|
||||
}
|
||||
if dead {
|
||||
spans.push(Span::styled(" exited", muted));
|
||||
tail.push(Span::styled(" exited", muted));
|
||||
}
|
||||
if let Some(detail) = s.activity_detail.as_deref().filter(|d| !d.is_empty()) {
|
||||
let head_w: usize = spans.iter().map(|sp| sp.width()).sum();
|
||||
let tail_w: usize = tail.iter().map(|sp| sp.width()).sum();
|
||||
let budget = width.saturating_sub(head_w + tail_w);
|
||||
spans.extend(fit_to_width(
|
||||
vec![Span::styled(format!(" {detail}"), muted)],
|
||||
budget,
|
||||
));
|
||||
}
|
||||
spans.extend(tail);
|
||||
Some(fit_to_width(spans, width))
|
||||
}
|
||||
}
|
||||
@@ -531,6 +545,93 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// #112: the agent-reported detail sits right after the duration on the
|
||||
/// meta line, in the muted style (docs/SIDEBAR.md).
|
||||
#[test]
|
||||
fn errored_session_shows_its_reported_detail_on_the_meta_line() {
|
||||
let mut s = session_named(1, "claude");
|
||||
s.activity = ActivityState::Error;
|
||||
s.activity_since = Utc::now() - Duration::seconds(90);
|
||||
s.activity_detail = Some("cargo build failed".into());
|
||||
let sb = Sidebar {
|
||||
sessions: vec![s],
|
||||
highlight: None,
|
||||
attached: Default::default(),
|
||||
};
|
||||
let all = rows(&draw(&sb, 40, 8));
|
||||
let idx = all.iter().position(|l| l.contains("claude")).unwrap();
|
||||
let meta = &all[idx + 1];
|
||||
assert!(meta.contains('●'), "meta line carries the dot: {meta:?}");
|
||||
assert!(
|
||||
meta.contains("1m"),
|
||||
"duration precedes the detail: {meta:?}"
|
||||
);
|
||||
assert!(
|
||||
meta.contains("cargo build failed"),
|
||||
"reported detail must render: {meta:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A fresh Error report earns line 2 on its own (contract rule 4), with
|
||||
/// the detail even though no duration has accrued.
|
||||
#[test]
|
||||
fn fresh_errored_session_earns_a_meta_line_for_its_detail() {
|
||||
let mut s = session_named(1, "claude");
|
||||
s.activity = ActivityState::Error;
|
||||
s.activity_since = Utc::now() - Duration::seconds(5);
|
||||
s.activity_detail = Some("waiting for approval".into());
|
||||
let sb = Sidebar {
|
||||
sessions: vec![s],
|
||||
highlight: None,
|
||||
attached: Default::default(),
|
||||
};
|
||||
let all = rows(&draw(&sb, 40, 8));
|
||||
let idx = all.iter().position(|l| l.contains("claude")).unwrap();
|
||||
assert!(
|
||||
all[idx + 1].contains("waiting for approval"),
|
||||
"detail must render on a fresh report's own meta line: {:?}",
|
||||
all[idx + 1]
|
||||
);
|
||||
}
|
||||
|
||||
/// #112 via docs/SIDEBAR.md: the detail truncates with `…` ahead of the
|
||||
/// trailing fragments — a long detail never pushes blocked work or
|
||||
/// `exited` off the line, and never wraps.
|
||||
#[test]
|
||||
fn long_detail_yields_to_blocked_work_and_exited() {
|
||||
let mut s = session_named(1, "claude");
|
||||
s.status = SessionStatus::Dead;
|
||||
s.activity_since = Utc::now() - Duration::seconds(5);
|
||||
s.activity_detail =
|
||||
Some("an absurdly long error message that cannot possibly fit ".repeat(3));
|
||||
s.blocked_work = Some(InTheWay {
|
||||
unpushed_commits: 2,
|
||||
..InTheWay::default()
|
||||
});
|
||||
let sb = Sidebar {
|
||||
sessions: vec![s],
|
||||
highlight: None,
|
||||
attached: Default::default(),
|
||||
};
|
||||
let all = rows(&draw(&sb, 40, 8));
|
||||
let idx = all.iter().position(|l| l.contains("claude")).unwrap();
|
||||
let meta = content(&all[idx + 1]);
|
||||
assert!(meta.contains('…'), "truncated detail ends with …: {meta:?}");
|
||||
assert!(
|
||||
meta.contains("2 unpushed commits"),
|
||||
"blocked work stays on the line: {meta:?}"
|
||||
);
|
||||
assert!(
|
||||
meta.contains("exited"),
|
||||
"exited fragment stays on the line: {meta:?}"
|
||||
);
|
||||
assert!(
|
||||
content(&all[idx + 2]).trim().is_empty(),
|
||||
"meta line must not wrap: {:?}",
|
||||
all[idx + 2]
|
||||
);
|
||||
}
|
||||
|
||||
/// docs/SIDEBAR.md: a payload predating #102 carries an epoch
|
||||
/// `activity_since` and so contributes no duration — a card must not claim
|
||||
/// the session has been idle since 1970. The dot still renders when some
|
||||
|
||||
Reference in New Issue
Block a user