From 5a9601dd3cb7e3d18e18432de10cef15b6257c18 Mon Sep 17 00:00:00 2001 From: dave Date: Tue, 7 Apr 2026 15:51:09 +0000 Subject: [PATCH] huskies: merge 495_bug_status_traffic_light_dots_use_unsupported_html_colouring_switch_to_emoji --- server/src/chat/commands/mod.rs | 5 +- server/src/chat/commands/status.rs | 143 ++++------------------------- 2 files changed, 22 insertions(+), 126 deletions(-) diff --git a/server/src/chat/commands/mod.rs b/server/src/chat/commands/mod.rs index 867ab4f8..c1128954 100644 --- a/server/src/chat/commands/mod.rs +++ b/server/src/chat/commands/mod.rs @@ -223,11 +223,10 @@ pub fn try_handle_command_with_html( Some((c, a)) => (c, a.trim()), None => (trimmed, ""), }; - // Only the no-arg status variant shows the pipeline with traffic-light - // dots; `status ` is a triage dump that needs no colour tags. + // Status command: emoji indicators render natively in all clients. if cmd_name.eq_ignore_ascii_case("status") && args.is_empty() { let body = status::build_pipeline_status(dispatch.project_root, dispatch.agents); - let html = status::build_pipeline_status_html(dispatch.project_root, dispatch.agents); + let html = plain_to_html(&body); return Some((body, html)); } } diff --git a/server/src/chat/commands/status.rs b/server/src/chat/commands/status.rs index 4fb3b33a..f047408b 100644 --- a/server/src/chat/commands/status.rs +++ b/server/src/chat/commands/status.rs @@ -65,25 +65,25 @@ fn read_story_blocked(project_root: &std::path::Path, stage_dir: &str, stem: &st .unwrap_or(false) } -/// Choose the traffic-light dot for a work item. +/// Choose the traffic-light indicator for a work item. /// /// Priority: blocked > throttled > running > idle. -/// Uses compact Unicode characters (not large emoji) so the output stays -/// readable in plain-text chat clients. +/// Uses coloured emoji so indicators render natively in all Matrix clients +/// (Element X and others do not support `` HTML tags). /// -/// - `●` running normally (active agent, no throttle) -/// - `◑` throttled (rate-limit warning received) -/// - `✗` hard-blocked (retry limit exceeded) -/// - `○` idle / no active agent +/// - 🔴 hard-blocked (retry limit exceeded) +/// - 🟠 throttled (rate-limit warning received) +/// - 🟢 running normally (active agent, no throttle) +/// - ⚪ idle / no active agent pub(super) fn traffic_light_dot(blocked: bool, throttled: bool, has_agent: bool) -> &'static str { if blocked { - "\u{2717} " // ✗ — hard blocked + "\u{1F534} " // 🔴 — hard blocked } else if throttled { - "\u{25D1} " // ◑ — throttled + "\u{1F7E0} " // 🟠 — throttled } else if has_agent { - "\u{25CF} " // ● — running normally + "\u{1F7E2} " // 🟢 — running normally } else { - "\u{25CB} " // ○ — idle / no agent + "\u{26AA} " // ⚪ — idle / no agent } } @@ -122,34 +122,6 @@ fn read_stage_items( items } -/// Build the HTML `formatted_body` for the pipeline status with Matrix colour -/// tags on the traffic-light dots. -/// -/// Converts the plain-text pipeline status (Markdown) to HTML via -/// pulldown-cmark and wraps each traffic-light character in a -/// `` tag so Matrix clients display them in -/// colour. -pub(super) fn build_pipeline_status_html(project_root: &std::path::Path, agents: &AgentPool) -> String { - use pulldown_cmark::{Options, Parser, html}; - - let plain = build_pipeline_status(project_root, agents); - let normalized = crate::chat::util::normalize_line_breaks(&plain); - let options = Options::ENABLE_TABLES - | Options::ENABLE_FOOTNOTES - | Options::ENABLE_STRIKETHROUGH - | Options::ENABLE_TASKLISTS; - let parser = Parser::new_ext(&normalized, options); - let mut html_out = String::new(); - html::push_html(&mut html_out, parser); - - // Wrap each traffic-light character with a Matrix colour tag. - html_out - .replace('\u{2717}', "\u{2717}") // ✗ blocked - .replace('\u{25D1}', "\u{25D1}") // ◑ throttled - .replace('\u{25CF}', "\u{25CF}") // ● running - .replace('\u{25CB}', "\u{25CB}") // ○ idle -} - /// Build the full pipeline status text formatted for Matrix (markdown). pub(super) fn build_pipeline_status(project_root: &std::path::Path, agents: &AgentPool) -> String { // Build a map from story_id → active AgentInfo for quick lookup. @@ -557,104 +529,29 @@ mod tests { // -- traffic_light_dot -------------------------------------------------- - // -- build_pipeline_status_html (colored dots) -------------------------- - - #[test] - fn html_status_colors_idle_dot_grey() { - use std::io::Write; - use tempfile::TempDir; - - let tmp = TempDir::new().unwrap(); - let stage_dir = tmp.path().join(".huskies/work/2_current"); - std::fs::create_dir_all(&stage_dir).unwrap(); - - let story_path = stage_dir.join("42_story_idle.md"); - let mut f = std::fs::File::create(&story_path).unwrap(); - writeln!(f, "---\nname: Idle Story\n---\n").unwrap(); - - let agents = AgentPool::new_test(3000); - let html = build_pipeline_status_html(tmp.path(), &agents); - - assert!( - html.contains("\u{25CB}"), - "idle dot should be grey (#888888): {html}" - ); - } - - #[test] - fn html_status_colors_blocked_dot_red() { - use std::io::Write; - use tempfile::TempDir; - - let tmp = TempDir::new().unwrap(); - let stage_dir = tmp.path().join(".huskies/work/2_current"); - std::fs::create_dir_all(&stage_dir).unwrap(); - - let story_path = stage_dir.join("42_story_blocked.md"); - let mut f = std::fs::File::create(&story_path).unwrap(); - writeln!(f, "---\nname: Blocked Story\nblocked: true\n---\n").unwrap(); - - let agents = AgentPool::new_test(3000); - let html = build_pipeline_status_html(tmp.path(), &agents); - - assert!( - html.contains("\u{2717}"), - "blocked dot should be red (#cc0000): {html}" - ); - } - - #[test] - fn html_status_plain_text_body_unchanged() { - use std::io::Write; - use tempfile::TempDir; - - let tmp = TempDir::new().unwrap(); - let stage_dir = tmp.path().join(".huskies/work/2_current"); - std::fs::create_dir_all(&stage_dir).unwrap(); - - let story_path = stage_dir.join("42_story_idle.md"); - let mut f = std::fs::File::create(&story_path).unwrap(); - writeln!(f, "---\nname: Idle Story\n---\n").unwrap(); - - let agents = AgentPool::new_test(3000); - let plain = build_pipeline_status(tmp.path(), &agents); - - // Plain text must still use bare Unicode dots (no HTML tags). - assert!( - plain.contains('\u{25CB}'), - "plain text should have bare Unicode idle dot: {plain}" - ); - assert!( - !plain.contains("data-mx-color"), - "plain text must not contain HTML colour attributes: {plain}" - ); - } - - // -- traffic_light_dot -------------------------------------------------- - #[test] fn dot_idle_when_no_agent() { - assert_eq!(traffic_light_dot(false, false, false), "\u{25CB} "); // ○ + assert_eq!(traffic_light_dot(false, false, false), "\u{26AA} "); // ⚪ } #[test] fn dot_running_when_agent_not_throttled() { - assert_eq!(traffic_light_dot(false, false, true), "\u{25CF} "); // ● + assert_eq!(traffic_light_dot(false, false, true), "\u{1F7E2} "); // 🟢 } #[test] fn dot_throttled_when_agent_throttled() { - assert_eq!(traffic_light_dot(false, true, true), "\u{25D1} "); // ◑ + assert_eq!(traffic_light_dot(false, true, true), "\u{1F7E0} "); // 🟠 } #[test] fn dot_blocked_takes_priority_over_throttled() { - assert_eq!(traffic_light_dot(true, true, true), "\u{2717} "); // ✗ + assert_eq!(traffic_light_dot(true, true, true), "\u{1F534} "); // 🔴 } #[test] fn dot_blocked_when_no_agent_but_blocked_flag() { - assert_eq!(traffic_light_dot(true, false, false), "\u{2717} "); // ✗ + assert_eq!(traffic_light_dot(true, false, false), "\u{1F534} "); // 🔴 } // -- read_story_blocked -------------------------------------------------- @@ -706,8 +603,8 @@ mod tests { let output = build_pipeline_status(tmp.path(), &agents); assert!( - output.contains("\u{25CB} "), // ○ - "idle story should show empty-circle dot: {output}" + output.contains("\u{26AA} "), // ⚪ + "idle story should show white circle emoji: {output}" ); } @@ -728,8 +625,8 @@ mod tests { let output = build_pipeline_status(tmp.path(), &agents); assert!( - output.contains("\u{2717} "), // ✗ - "blocked story should show X dot: {output}" + output.contains("\u{1F534} "), // 🔴 + "blocked story should show red circle emoji: {output}" ); } }