fix(maestro): suppress epoch durations on TUI cards

This commit is contained in:
Miguel Palhas
2026-08-24 19:57:58 +01:00
parent 78cddec730
commit 766d256970
+64 -6
View File
@@ -19,6 +19,12 @@ use uuid::Uuid;
/// Time-in-state alone earns a meta line only once the session has sat in
/// its current activity state this long (docs/SIDEBAR.md).
const META_LINE_AFTER_SECS: i64 = 30;
/// A payload predating #102 defaults `activity_since` to the Unix epoch. Per
/// docs/SIDEBAR.md such a session contributes no duration — the dot still
/// renders — so anything implausibly old counts as "never observed" rather
/// than a card announcing that the session has been idle for half a century.
/// Mirrors `PLAUSIBLE_EPOCH_MS` in `web/src/activity.ts`.
const PLAUSIBLE_SINCE_YEAR: i32 = 2010;
const INDENT: &str = " ";
const META_INDENT: &str = " ";
/// Spawn-similar and kill, shown on the highlighted card's top-right corner.
@@ -129,25 +135,31 @@ impl Sidebar {
/// The adaptive second line: `<dot> <duration> [<blocked work> | exited]`.
fn meta_row(&self, s: &Session, width: usize) -> Option<Vec<Span<'static>>> {
let now = Utc::now();
let held_secs = (now - s.activity_since).num_seconds();
let held = held_for(s, now);
let held_secs = held.map(|d| d.num_seconds()).unwrap_or(0);
// #83: a worktree that blocks a kill is summarised here rather than
// on line 1; the kill confirm names the specifics.
let blocked = s.blocked_work.filter(|b| !b.is_clear());
let dead = s.status == SessionStatus::Dead;
let errored = s.activity == ActivityState::Error;
if held_secs < META_LINE_AFTER_SECS && blocked.is_none() && !dead && !errored {
if (held.is_none() || held_secs < META_LINE_AFTER_SECS)
&& blocked.is_none()
&& !dead
&& !errored
{
return None;
}
let base = card_base(self.highlight == Some(s.id));
let dot_style = base.patch(Style::default().fg(activity_color(s.activity)));
let muted = Style::default().bg(base.bg.unwrap_or(Color::Reset));
let duration = format_state_duration(now - s.activity_since);
let mut spans = vec![
Span::styled(META_INDENT, base),
Span::styled("●", dot_style),
Span::styled(" ", base),
Span::styled(duration, dot_style),
];
if let Some(d) = held {
spans.push(Span::styled(" ", base));
spans.push(Span::styled(format_state_duration(d), dot_style));
}
if let Some(b) = blocked {
spans.push(Span::styled(format!(" ⚠ {b}"), muted));
}
@@ -171,6 +183,16 @@ fn activity_color(state: ActivityState) -> Color {
/// docs/SIDEBAR.md duration format — floored whole seconds / minutes / hours,
/// measured in the current activity state. No days, no rounding up.
/// How long the session has held its current activity state, or `None` when
/// the daemon never reported one (see [`PLAUSIBLE_SINCE_YEAR`]).
fn held_for(s: &Session, now: chrono::DateTime<Utc>) -> Option<chrono::Duration> {
use chrono::Datelike;
if s.activity_since.year() < PLAUSIBLE_SINCE_YEAR {
return None;
}
Some(now - s.activity_since)
}
fn format_state_duration(elapsed: chrono::Duration) -> String {
let secs = elapsed.num_seconds().max(0);
match secs {
@@ -228,7 +250,7 @@ fn name_for(s: &Session) -> String {
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
use chrono::{DateTime, Duration};
use maestro_protocol::InTheWay;
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
@@ -508,6 +530,42 @@ mod tests {
);
}
/// 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
/// other trigger puts line 2 on screen.
#[test]
fn epoch_activity_since_renders_no_duration() {
let mut legacy = session_named(1, "claude");
legacy.activity_since = DateTime::from_timestamp(0, 0).unwrap();
legacy.status = SessionStatus::Dead;
let sb = Sidebar {
sessions: vec![legacy],
highlight: None,
attached: Default::default(),
};
let rendered = render_to_string(&draw(&sb, 40, 8));
assert!(
rendered.contains("exited"),
"dead session must still show line 2:\n{rendered}"
);
assert!(
!rendered.contains('h') || !rendered.contains("● "),
"no duration may follow the dot for an epoch stamp:\n{rendered}"
);
for line in rendered.lines().filter(|l| l.contains('●')) {
assert!(
!line
.split('●')
.nth(1)
.unwrap_or("")
.trim_start()
.starts_with(char::is_numeric),
"duration rendered from an epoch stamp: {line:?}"
);
}
}
#[test]
fn falls_back_to_command_when_name_metadata_is_absent() {
let s = Session {