story-kit: merge 313_story_improve_htop_output_formatting_for_mobile_matrix_clients

This commit is contained in:
Dave
2026-03-19 19:44:00 +00:00
parent e6eaa10c16
commit 1739c2ff58

View File

@@ -201,6 +201,10 @@ fn gather_process_stats(worktree_path: &str) -> Option<AgentProcessStats> {
/// ///
/// `tick` is the number of updates sent so far (0 = initial). /// `tick` is the number of updates sent so far (0 = initial).
/// `total_duration_secs` is the configured auto-stop timeout. /// `total_duration_secs` is the configured auto-stop timeout.
///
/// Output uses a compact single-line format per agent so it renders
/// without wrapping on narrow screens (~40 chars), such as mobile
/// Matrix clients.
pub fn build_htop_message(agents: &AgentPool, tick: u32, total_duration_secs: u64) -> String { pub fn build_htop_message(agents: &AgentPool, tick: u32, total_duration_secs: u64) -> String {
let elapsed_secs = (tick as u64) * 5; let elapsed_secs = (tick as u64) * 5;
let remaining_secs = total_duration_secs.saturating_sub(elapsed_secs); let remaining_secs = total_duration_secs.saturating_sub(elapsed_secs);
@@ -210,11 +214,11 @@ pub fn build_htop_message(agents: &AgentPool, tick: u32, total_duration_secs: u6
let load = get_load_average(); let load = get_load_average();
let mut lines = vec![ let mut lines = vec![
format!("**htop** — {load}"),
format!( format!(
"*Updates every 5s · auto-stops in {}m{}s · send `htop stop` to stop*", "**htop** · auto-stops in {}m{}s",
remaining_mins, remaining_secs_rem remaining_mins, remaining_secs_rem
), ),
load,
String::new(), String::new(),
]; ];
@@ -227,8 +231,6 @@ pub fn build_htop_message(agents: &AgentPool, tick: u32, total_duration_secs: u6
if active.is_empty() { if active.is_empty() {
lines.push("*No agents currently running.*".to_string()); lines.push("*No agents currently running.*".to_string());
} else { } else {
lines.push("| Agent | Story | CPU% | MEM% | Procs |".to_string());
lines.push("|-------|-------|-----:|-----:|------:|".to_string());
for agent in &active { for agent in &active {
let story_label = agent let story_label = agent
.story_id .story_id
@@ -242,12 +244,8 @@ pub fn build_htop_message(agents: &AgentPool, tick: u32, total_duration_secs: u6
.and_then(gather_process_stats) .and_then(gather_process_stats)
.unwrap_or_default(); .unwrap_or_default();
lines.push(format!( lines.push(format!(
"| {} | {} | {:.1} | {:.1} | {} |", "**{}** #{} cpu:{:.1}% mem:{:.1}%",
agent.agent_name, agent.agent_name, story_label, stats.cpu_pct, stats.mem_pct,
story_label,
stats.cpu_pct,
stats.mem_pct,
stats.num_procs,
)); ));
} }
} }
@@ -569,4 +567,55 @@ mod tests {
"should show remaining time: {text}" "should show remaining time: {text}"
); );
} }
#[test]
fn build_htop_message_load_on_own_line() {
// Load average must be on its own line, not combined with the htop header.
let pool = Arc::new(crate::agents::AgentPool::new_test(3000));
let text = build_htop_message(&pool, 0, 300);
let lines: Vec<&str> = text.lines().collect();
let header_line = lines.first().expect("should have a header line");
// Header line must NOT contain "load" — load is on the second line.
assert!(
!header_line.contains("load"),
"load should be on its own line, not the header: {header_line}"
);
// Second line must contain "load".
let load_line = lines.get(1).expect("should have a load line");
assert!(
load_line.contains("load"),
"second line should contain load info: {load_line}"
);
}
#[test]
fn build_htop_message_no_table_syntax() {
// Must not use Markdown table format (pipes/separators) — those are too
// wide for narrow mobile screens.
let pool = Arc::new(crate::agents::AgentPool::new_test(3000));
let text = build_htop_message(&pool, 0, 300);
assert!(
!text.contains("|----"),
"output must not contain table separator rows: {text}"
);
assert!(
!text.contains("| Agent"),
"output must not contain table header row: {text}"
);
}
#[test]
fn build_htop_message_header_fits_40_chars() {
// The header line (htop + remaining time) must fit in ~40 rendered chars.
let pool = Arc::new(crate::agents::AgentPool::new_test(3000));
let text = build_htop_message(&pool, 0, 300);
let header = text.lines().next().expect("should have a header line");
// Strip markdown bold markers (**) for length calculation.
let rendered = header.replace("**", "");
assert!(
rendered.len() <= 40,
"header line too wide for mobile ({} chars): {rendered}",
rendered.len()
);
}
} }