story-kit: merge 313_story_improve_htop_output_formatting_for_mobile_matrix_clients
This commit is contained in:
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user