fix(maestro): suppress epoch durations on TUI cards
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user