pub fn format_clock(seconds: u64) -> String {
const MINUTE: u64 = 60;
const HOUR: u64 = 60 * MINUTE;
const DAY: u64 = 24 * HOUR;
match seconds {
seconds if seconds < MINUTE => format!("{seconds}s"),
seconds if seconds < HOUR => format!("{}m{:02}s", seconds / MINUTE, seconds % MINUTE),
seconds if seconds < DAY => format!("{}h{:02}m", seconds / HOUR, (seconds % HOUR) / MINUTE),
seconds => format!("{}d{:02}h", seconds / DAY, (seconds % DAY) / HOUR),
}
}
pub fn format_turn_clock(now_epoch_seconds: u64, current_turn_started_at: Option<u64>) -> String {
match current_turn_started_at {
Some(started_at) => format_clock(now_epoch_seconds.saturating_sub(started_at)),
None => "[idle]".into(),
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SessionActivity {
pub pursuing_goal: bool,
pub capacity_retry: Option<mj_core::relay::CapacityRetry>,
pub activity_turn_started_at_ms: Option<i64>,
pub prompt_in_flight: bool,
pub execution: Option<mj_core::relay::RelayExecutionState>,
pub idle_since_ms: Option<i64>,
pub harness_turn_started_at_ms: Option<i64>,
pub foreground_tool_started_at_ms: Option<i64>,
pub background_commands: Vec<mj_core::relay::BackgroundCommand>,
pub active_user_shells: Vec<mj_core::relay::ActiveUserShell>,
}
impl SessionActivity {
pub fn of(operational: &mj_core::relay::RelayOperationalState) -> Self {
Self {
pursuing_goal: operational.goal.active(),
capacity_retry: operational.capacity_retry.clone(),
activity_turn_started_at_ms: operational
.active_prompt
.as_ref()
.map(|prompt| prompt.started_at_ms)
.or(operational.activity_turn_started_at_ms),
prompt_in_flight: operational.active_prompt.is_some(),
execution: Some(operational.execution),
idle_since_ms: operational.idle_since_ms,
harness_turn_started_at_ms: operational.harness_turn.map(|turn| turn.started_at_ms),
foreground_tool_started_at_ms: operational.foreground_tool_started_at_ms.or_else(
|| {
(operational.execution == mj_core::relay::RelayExecutionState::Running)
.then_some(operational.current_step_started_at_ms)
.flatten()
.filter(|timestamp| *timestamp >= 0)
},
),
background_commands: operational.background_commands.clone(),
active_user_shells: operational.active_user_shells.clone(),
}
}
#[must_use]
pub fn is_idle(&self, current_turn_started_at: Option<u64>) -> bool {
!self.pursuing_goal
&& self.capacity_retry.is_none()
&& matches!(
self.kind(current_turn_started_at),
SessionActivityKind::Idle
)
}
pub fn is_working(
&self,
current_turn_started_at: Option<u64>,
waiting_for_input: bool,
) -> bool {
match self.kind(current_turn_started_at) {
SessionActivityKind::Idle | SessionActivityKind::Goal => false,
SessionActivityKind::Lifecycle => {
self.execution == Some(mj_core::relay::RelayExecutionState::Closing)
}
SessionActivityKind::Background => true,
SessionActivityKind::Turn | SessionActivityKind::Step => {
!waiting_for_input
|| !self.background_commands.is_empty()
|| !self.active_user_shells.is_empty()
}
}
}
pub fn display_clock(
&self,
now_epoch_seconds: u64,
current_turn_started_at: Option<u64>,
current_step_started_at_ms: Option<u64>,
detailed: bool,
) -> String {
if let Some(retry) = &self.capacity_retry {
return retry
.status(now_epoch_seconds.saturating_mul(1000).min(i64::MAX as u64) as i64);
}
let kind = self.kind(current_turn_started_at);
if kind == SessionActivityKind::Goal {
return "Pursuing goal".into();
}
if kind == SessionActivityKind::Idle {
return "Idle".into();
}
if kind == SessionActivityKind::Lifecycle {
return self.lifecycle_label().into();
}
let turn = current_turn_started_at
.or_else(|| self.harness_turn_since())
.or_else(|| self.activity_turn_started_at_ms.and_then(epoch_seconds));
if detailed {
return match kind {
SessionActivityKind::Turn => {
let step = current_step_started_at_ms
.map(|stamp| stamp / 1_000)
.or(turn)
.map(|step| turn.map_or(step, |turn| step.max(turn)));
format!(
"{} {}",
elapsed_label("T", now_epoch_seconds, turn),
elapsed_label("S", now_epoch_seconds, step)
)
}
SessionActivityKind::Step => {
elapsed_label("S", now_epoch_seconds, self.foreground_tool_since())
}
SessionActivityKind::Background => {
elapsed_label("BG", now_epoch_seconds, self.background_since())
}
_ => unreachable!(),
};
}
let started = match kind {
SessionActivityKind::Turn => turn,
SessionActivityKind::Step => turn.or_else(|| self.foreground_tool_since()),
SessionActivityKind::Background => self.background_since(),
_ => None,
};
let label = if kind == SessionActivityKind::Background {
let tasks = self.background_commands.len();
let shells = self.active_user_shells.len();
let mut counts = Vec::new();
if tasks > 0 {
counts.push(format!("{tasks} task{}", if tasks == 1 { "" } else { "s" }));
}
if shells > 0 {
counts.push(format!(
"{shells} shell{}",
if shells == 1 { "" } else { "s" }
));
}
counts.join(", ")
} else {
"Working".to_owned()
};
elapsed_label(&label, now_epoch_seconds, started)
}
#[must_use]
pub fn details(
&self,
current_turn_started_at_ms: Option<i64>,
current_step_started_at_ms: Option<i64>,
) -> SessionActivityDetails {
let kind = self.kind(current_turn_started_at_ms.map(|_| 0));
let turn_started_at_ms = match kind {
SessionActivityKind::Turn => current_turn_started_at_ms
.or(self.harness_turn_started_at_ms)
.filter(|timestamp| *timestamp >= 0),
_ => None,
};
let step_started_at_ms = match kind {
SessionActivityKind::Turn => current_step_started_at_ms
.filter(|timestamp| *timestamp >= 0)
.or(turn_started_at_ms)
.zip(turn_started_at_ms)
.map(|(step, turn)| step.max(turn)),
SessionActivityKind::Step => self
.foreground_tool_started_at_ms
.filter(|timestamp| *timestamp >= 0),
_ => None,
};
let background_started_at_ms = (kind == SessionActivityKind::Background)
.then(|| {
self.background_commands
.iter()
.map(|command| command.started_at_ms)
.chain(
self.active_user_shells
.iter()
.filter_map(|shell| shell.started_at_ms),
)
.filter(|timestamp| *timestamp >= 0)
.min()
})
.flatten();
let label = match kind {
SessionActivityKind::Lifecycle => Some(self.lifecycle_label().to_owned()),
SessionActivityKind::Goal => Some("Pursuing goal".into()),
_ => None,
};
SessionActivityDetails {
kind,
turn_started_at_ms,
step_started_at_ms,
background_started_at_ms,
idle_since_ms: (kind == SessionActivityKind::Idle)
.then_some(self.idle_since_ms)
.flatten()
.filter(|timestamp| *timestamp >= 0),
label,
}
}
fn kind(&self, current_turn_started_at: Option<u64>) -> SessionActivityKind {
match self.execution {
Some(mj_core::relay::RelayExecutionState::Closing) => {
return SessionActivityKind::Lifecycle;
}
Some(mj_core::relay::RelayExecutionState::Closed) => {
return SessionActivityKind::Lifecycle;
}
Some(
mj_core::relay::RelayExecutionState::Idle
| mj_core::relay::RelayExecutionState::Running,
)
| None => {}
}
if current_turn_started_at.is_some()
|| self.harness_turn_started_at_ms.is_some()
|| self.prompt_in_flight
{
return SessionActivityKind::Turn;
}
if self.foreground_tool_started_at_ms.is_some() {
return SessionActivityKind::Step;
}
if !self.background_commands.is_empty() || !self.active_user_shells.is_empty() {
return SessionActivityKind::Background;
}
if self.pursuing_goal {
SessionActivityKind::Goal
} else {
SessionActivityKind::Idle
}
}
fn lifecycle_label(&self) -> &'static str {
match self.execution {
Some(mj_core::relay::RelayExecutionState::Closing) => "Closing",
Some(mj_core::relay::RelayExecutionState::Closed) => "Closed",
_ => "Lifecycle",
}
}
fn harness_turn_since(&self) -> Option<u64> {
epoch_seconds(self.harness_turn_started_at_ms?)
}
fn foreground_tool_since(&self) -> Option<u64> {
epoch_seconds(self.foreground_tool_started_at_ms?)
}
fn background_since(&self) -> Option<u64> {
self.background_commands
.iter()
.filter_map(|command| epoch_seconds(command.started_at_ms))
.chain(
self.active_user_shells
.iter()
.filter_map(|shell| shell.started_at_ms)
.filter_map(epoch_seconds),
)
.min()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionActivityKind {
Goal,
Turn,
Step,
Background,
Lifecycle,
Idle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionActivityDetails {
pub kind: SessionActivityKind,
pub turn_started_at_ms: Option<i64>,
pub step_started_at_ms: Option<i64>,
pub background_started_at_ms: Option<i64>,
pub idle_since_ms: Option<i64>,
pub label: Option<String>,
}
fn epoch_seconds(timestamp_ms: i64) -> Option<u64> {
u64::try_from(timestamp_ms).ok().map(|value| value / 1_000)
}
fn elapsed_label(label: &str, now_epoch_seconds: u64, started_at: Option<u64>) -> String {
started_at.map_or_else(
|| label.to_owned(),
|started_at| {
format!(
"{label} {}",
format_clock(now_epoch_seconds.saturating_sub(started_at))
)
},
)
}
pub fn format_activity_columns(
now_epoch_seconds: u64,
current_turn_started_at: Option<u64>,
current_step_started_at_ms: Option<u64>,
activity: &SessionActivity,
) -> Vec<String> {
if let Some(retry) = &activity.capacity_retry {
return vec![
retry.status(now_epoch_seconds.saturating_mul(1000).min(i64::MAX as u64) as i64),
];
}
match activity.kind(current_turn_started_at) {
SessionActivityKind::Turn => {
let turn_started = current_turn_started_at.or_else(|| activity.harness_turn_since());
let step_started = current_step_started_at_ms
.map(|value| value / 1_000)
.or(turn_started)
.zip(turn_started)
.map(|(step, turn)| step.max(turn));
vec![
elapsed_label("Turn", now_epoch_seconds, turn_started),
elapsed_label("Step", now_epoch_seconds, step_started),
]
}
SessionActivityKind::Step => {
vec![elapsed_label(
"Step",
now_epoch_seconds,
activity.foreground_tool_since(),
)]
}
SessionActivityKind::Background => {
vec![elapsed_label(
" BG",
now_epoch_seconds,
activity.background_since(),
)]
}
SessionActivityKind::Lifecycle => vec![activity.lifecycle_label().to_owned()],
SessionActivityKind::Idle => vec!["[idle]".into()],
SessionActivityKind::Goal => vec!["Pursuing goal".into()],
}
}
pub fn format_activity_clock(
now_epoch_seconds: u64,
current_turn_started_at: Option<u64>,
activity: &SessionActivity,
) -> String {
if let Some(retry) = &activity.capacity_retry {
return retry.status(now_epoch_seconds.saturating_mul(1000).min(i64::MAX as u64) as i64);
}
match activity.kind(current_turn_started_at) {
SessionActivityKind::Turn => {
if current_turn_started_at.is_some() {
format_turn_clock(now_epoch_seconds, current_turn_started_at)
} else {
format!(
"[{}]",
elapsed_label("Turn", now_epoch_seconds, activity.harness_turn_since())
)
}
}
SessionActivityKind::Step => format!(
"[{}]",
elapsed_label("Step", now_epoch_seconds, activity.foreground_tool_since(),)
),
SessionActivityKind::Background => format!(
"[{}]",
elapsed_label("BG", now_epoch_seconds, activity.background_since())
),
SessionActivityKind::Lifecycle => format!("[{}]", activity.lifecycle_label()),
SessionActivityKind::Idle => "[idle]".into(),
SessionActivityKind::Goal => "Pursuing goal".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn claude_background_agents_render_bg_until_the_live_set_is_empty() {
let temp = tempfile::tempdir().unwrap();
let mut relay = mj_worker::relay::DurableRelay::open(
temp.path(),
"018f9dd2-a3b4-7c8d-9000-123456789abc",
"1.0.0",
)
.unwrap();
relay.set_background_work_policy(mj_worker::relay::BackgroundWorkPolicy::ClaudeTasks);
relay
.claude_background_tasks_changed(vec![mj_core::acp::ClaudeBackgroundTask {
task_id: "design-review".into(),
description: "Design simplification and cleanup review".into(),
}])
.unwrap();
let state = relay.operational_state();
let activity = SessionActivity::of(&state);
let now = (state.background_commands[0].started_at_ms / 1_000) as u64 + 60;
assert!(!activity.is_idle(None));
assert_eq!(format_activity_clock(now, None, &activity), "[BG 1m00s]");
assert_eq!(
format_activity_columns(now, None, None, &activity),
vec![" BG 1m00s"]
);
relay.claude_background_tasks_changed(Vec::new()).unwrap();
let activity = SessionActivity::of(&relay.operational_state());
assert!(activity.is_idle(None));
assert_eq!(format_activity_clock(now, None, &activity), "[idle]");
}
#[test]
fn running_clocks_read_as_the_two_largest_units_that_fit() {
for (seconds, expected) in [
(0, "0s"),
(36, "36s"),
(59, "59s"),
(60, "1m00s"),
(2_616, "43m36s"),
(3_599, "59m59s"),
(3_600, "1h00m"),
(6_180, "1h43m"),
(86_399, "23h59m"),
(86_400, "1d00h"),
(183_600, "2d03h"),
] {
assert_eq!(format_clock(seconds), expected, "{seconds} seconds");
}
}
#[test]
fn a_live_sdk_step_proves_work_but_an_idle_step_clock_does_not() {
let temp = tempfile::tempdir().unwrap();
let relay =
mj_worker::relay::DurableRelay::open(temp.path(), "step-session", "test").unwrap();
let mut state = relay.operational_state();
state.execution = mj_core::relay::RelayExecutionState::Running;
state.current_step_started_at_ms = Some(50_000);
let activity = SessionActivity::of(&state);
assert!(activity.is_working(None, false));
assert_eq!(activity.display_clock(60, None, None, false), "Working 10s");
state.execution = mj_core::relay::RelayExecutionState::Idle;
assert!(!SessionActivity::of(&state).is_working(None, false));
state.execution = mj_core::relay::RelayExecutionState::Running;
state.current_step_started_at_ms = None;
assert!(!SessionActivity::of(&state).is_working(None, false));
}
#[test]
fn turn_clock_formats_running_periods_and_marks_idle_sessions() {
assert_eq!(format_turn_clock(500, Some(375)), "2m05s");
assert_eq!(format_turn_clock(400_000, Some(1_000)), "4d14h");
assert_eq!(format_turn_clock(5_000, None), "[idle]");
}
#[test]
fn active_goal_between_turns_is_not_idle_or_computation() {
let activity = SessionActivity {
pursuing_goal: true,
..Default::default()
};
assert!(!activity.is_idle(None));
assert!(!activity.is_working(None, false));
assert_eq!(
activity.display_clock(100, None, None, false),
"Pursuing goal"
);
assert_eq!(activity.details(None, None).kind, SessionActivityKind::Goal);
}
fn background(started_at_ms: i64, command: &str) -> SessionActivity {
SessionActivity {
pursuing_goal: false,
capacity_retry: None,
execution: None,
activity_turn_started_at_ms: None,
prompt_in_flight: false,
idle_since_ms: None,
harness_turn_started_at_ms: None,
foreground_tool_started_at_ms: None,
background_commands: vec![mj_core::relay::BackgroundCommand {
id: "test-background".into(),
started_at_ms,
command: command.to_owned(),
can_stop: false,
}],
active_user_shells: Vec::new(),
}
}
#[test]
fn a_foreground_tool_takes_precedence_over_older_background_work() {
let mut activity = background(17_384_000, "cargo test --old");
activity.foreground_tool_started_at_ms = Some(19_900_000);
assert_eq!(
format_activity_columns(20_000, None, None, &activity),
vec!["Step 1m40s".to_owned()]
);
assert_eq!(
format_activity_clock(20_000, None, &activity),
"[Step 1m40s]"
);
}
#[test]
fn a_session_row_reads_as_its_turn_its_background_work_or_idle() {
let idle = SessionActivity::default();
let waiting = background(17_384_000, "cargo test");
for (label, activity, turn_started, columns, cell) in [
(
"running",
&idle,
Some(17_384_u64),
vec!["Turn 43m36s".to_owned(), "Step 12s".to_owned()],
"43m36s",
),
(
"background",
&waiting,
None,
vec![" BG 43m36s".to_owned()],
"[BG 43m36s]",
),
("idle", &idle, None, vec!["[idle]".to_owned()], "[idle]"),
] {
assert_eq!(
format_activity_columns(20_000, turn_started, Some(19_988_000), activity),
columns,
"{label} columns"
);
assert_eq!(
format_activity_clock(20_000, turn_started, activity),
cell,
"{label} cell"
);
}
}
#[test]
fn a_row_reads_its_step_as_the_whole_turn_when_no_step_is_reported() {
assert_eq!(
format_activity_columns(20_000, Some(17_384), None, &SessionActivity::default()),
vec!["Turn 43m36s".to_owned(), "Step 43m36s".to_owned()]
);
}
#[test]
fn a_step_that_predates_its_turn_reads_from_the_turn_start() {
assert_eq!(
format_activity_columns(
20_000,
Some(19_900),
Some(17_384_000),
&SessionActivity::default()
),
vec!["Turn 1m40s".to_owned(), "Step 1m40s".to_owned()]
);
}
#[test]
fn structured_activity_clamps_steps_to_their_turn() {
let activity = SessionActivity::default();
let details = activity.details(Some(20_000_000), Some(19_000_000));
assert_eq!(details.kind, SessionActivityKind::Turn);
assert_eq!(details.turn_started_at_ms, Some(20_000_000));
assert_eq!(details.step_started_at_ms, Some(20_000_000));
let details = activity.details(Some(19_000_000), Some(20_000_000));
assert_eq!(details.step_started_at_ms, Some(20_000_000));
}
#[test]
fn structured_activity_keeps_known_idle_and_missing_idle_since_distinct() {
let known = SessionActivity {
pursuing_goal: Default::default(),
execution: Some(mj_core::relay::RelayExecutionState::Idle),
idle_since_ms: Some(19_000_000),
..SessionActivity::default()
}
.details(None, None);
assert_eq!(known.kind, SessionActivityKind::Idle);
assert_eq!(known.idle_since_ms, Some(19_000_000));
let old_worker = SessionActivity {
pursuing_goal: Default::default(),
execution: Some(mj_core::relay::RelayExecutionState::Idle),
..SessionActivity::default()
}
.details(None, None);
assert_eq!(old_worker.kind, SessionActivityKind::Idle);
assert_eq!(old_worker.idle_since_ms, None);
}
#[test]
fn a_turn_hides_background_work_even_before_the_projection_catches_up() {
let mut activity = background(17_384_000, "cargo test");
activity.harness_turn_started_at_ms = Some(19_000_000);
assert_eq!(
format_activity_columns(20_000, None, None, &activity),
vec!["Turn 16m40s".to_owned(), "Step 16m40s".to_owned()],
"a row reports the harness turn while the projection catches up"
);
assert_eq!(
format_activity_clock(20_000, None, &activity),
"[Turn 16m40s]"
);
}
#[test]
fn idle_requires_no_projected_or_relay_activity() {
let mut activity = SessionActivity::default();
assert!(activity.is_idle(None));
assert!(!activity.is_idle(Some(20_000)));
activity.harness_turn_started_at_ms = Some(19_000_000);
assert!(!activity.is_idle(None));
activity.harness_turn_started_at_ms = None;
activity.foreground_tool_started_at_ms = Some(19_000_000);
assert!(!activity.is_idle(None));
activity.foreground_tool_started_at_ms = None;
activity.background_commands = vec![mj_core::relay::BackgroundCommand {
id: "test-background".into(),
started_at_ms: 19_000_000,
command: "cargo test".into(),
can_stop: false,
}];
assert!(!activity.is_idle(None));
}
#[test]
fn invalid_activity_timestamps_still_report_work_without_fabricating_a_clock() {
let foreground = SessionActivity {
pursuing_goal: Default::default(),
foreground_tool_started_at_ms: Some(-1),
..SessionActivity::default()
};
assert!(!foreground.is_idle(None));
assert_eq!(
format_activity_columns(20_000, None, None, &foreground),
vec!["Step".to_owned()]
);
assert_eq!(format_activity_clock(20_000, None, &foreground), "[Step]");
let background = SessionActivity {
pursuing_goal: Default::default(),
background_commands: vec![mj_core::relay::BackgroundCommand {
id: "test-background".into(),
started_at_ms: -1,
command: "cargo test".into(),
can_stop: false,
}],
..SessionActivity::default()
};
assert!(!background.is_idle(None));
assert_eq!(
format_activity_columns(20_000, None, None, &background),
vec![" BG".to_owned()]
);
assert_eq!(format_activity_clock(20_000, None, &background), "[BG]");
}
#[test]
fn relay_execution_and_user_shells_keep_activity_non_idle_without_timestamps() {
let running = SessionActivity {
pursuing_goal: Default::default(),
prompt_in_flight: true,
execution: Some(mj_core::relay::RelayExecutionState::Running),
..SessionActivity::default()
};
assert!(!running.is_idle(None));
assert_eq!(
format_activity_columns(20_000, None, None, &running),
vec!["Turn".to_owned(), "Step".to_owned()]
);
assert_eq!(format_activity_clock(20_000, None, &running), "[Turn]");
for execution in [
mj_core::relay::RelayExecutionState::Closing,
mj_core::relay::RelayExecutionState::Closed,
] {
let lifecycle = SessionActivity {
pursuing_goal: Default::default(),
execution: Some(execution),
..SessionActivity::default()
};
assert!(!lifecycle.is_idle(None));
let label = match execution {
mj_core::relay::RelayExecutionState::Closing => "Closing",
mj_core::relay::RelayExecutionState::Closed => "Closed",
_ => unreachable!(),
};
assert_eq!(
format_activity_columns(20_000, None, None, &lifecycle),
vec![label.to_owned()]
);
assert_eq!(
format_activity_clock(20_000, None, &lifecycle),
format!("[{label}]")
);
}
let shell = SessionActivity {
pursuing_goal: Default::default(),
active_user_shells: vec![mj_core::relay::ActiveUserShell {
command_id: "shell-1".into(),
command: "cargo test".into(),
created_at_ms: 20_000_000,
started_at_ms: None,
}],
..SessionActivity::default()
};
assert!(!shell.is_idle(None));
assert_eq!(
format_activity_columns(20_000, None, None, &shell),
vec![" BG".to_owned()]
);
assert_eq!(format_activity_clock(20_000, None, &shell), "[BG]");
}
#[test]
fn compact_clock_distinguishes_a_finished_turn_with_background_work() {
let mut activity = background(20_000, "build");
activity.activity_turn_started_at_ms = Some(10_000);
assert_eq!(
activity.display_clock(60, Some(10), Some(50_000), false),
"Working 50s"
);
assert_eq!(
activity.display_clock(60, Some(10), Some(50_000), true),
"T 50s S 10s"
);
assert_eq!(activity.display_clock(70, None, None, false), "1 task 50s");
assert_eq!(activity.display_clock(70, None, None, true), "BG 50s");
activity.activity_turn_started_at_ms = Some(65_000);
assert_eq!(
activity.display_clock(70, Some(65), None, false),
"Working 5s"
);
activity.background_commands.clear();
assert_eq!(activity.display_clock(70, None, None, false), "Idle");
}
#[test]
fn background_status_counts_tasks_and_user_shells_without_claiming_foreground_work() {
let mut activity = background(20_000, "build");
activity
.background_commands
.push(mj_core::relay::BackgroundCommand {
id: "second".into(),
started_at_ms: 30_000,
command: "test".into(),
can_stop: false,
});
assert_eq!(activity.display_clock(70, None, None, false), "2 tasks 50s");
activity
.active_user_shells
.push(mj_core::relay::ActiveUserShell {
command_id: "shell".into(),
command: "watch".into(),
created_at_ms: 40_000,
started_at_ms: Some(40_000),
});
assert_eq!(
activity.display_clock(70, None, None, false),
"2 tasks, 1 shell 50s"
);
activity.background_commands.clear();
assert_eq!(activity.display_clock(70, None, None, false), "1 shell 30s");
activity.foreground_tool_started_at_ms = Some(60_000);
assert_eq!(activity.display_clock(70, None, None, false), "Working 10s");
activity.foreground_tool_started_at_ms = None;
activity.active_user_shells.clear();
assert_eq!(activity.display_clock(70, None, None, false), "Idle");
}
#[test]
fn activity_indicators_ignore_stale_phases_and_questions_without_work() {
let mut activity = SessionActivity {
pursuing_goal: Default::default(),
execution: Some(mj_core::relay::RelayExecutionState::Running),
..SessionActivity::default()
};
assert!(!activity.is_working(None, false));
assert_eq!(activity.display_clock(60, None, None, false), "Idle");
activity.prompt_in_flight = true;
assert!(activity.is_working(None, false));
assert_eq!(activity.display_clock(60, None, None, false), "Working");
assert!(!activity.is_working(Some(10), true));
activity.background_commands = background(20_000, "build").background_commands;
assert!(activity.is_working(Some(10), true));
activity.execution = Some(mj_core::relay::RelayExecutionState::Closed);
assert!(!activity.is_working(Some(10), false));
assert_eq!(activity.display_clock(60, Some(10), None, false), "Closed");
}
}