huskies: merge 495_bug_status_traffic_light_dots_use_unsupported_html_colouring_switch_to_emoji
This commit is contained in:
@@ -223,11 +223,10 @@ pub fn try_handle_command_with_html(
|
|||||||
Some((c, a)) => (c, a.trim()),
|
Some((c, a)) => (c, a.trim()),
|
||||||
None => (trimmed, ""),
|
None => (trimmed, ""),
|
||||||
};
|
};
|
||||||
// Only the no-arg status variant shows the pipeline with traffic-light
|
// Status command: emoji indicators render natively in all clients.
|
||||||
// dots; `status <number>` is a triage dump that needs no colour tags.
|
|
||||||
if cmd_name.eq_ignore_ascii_case("status") && args.is_empty() {
|
if cmd_name.eq_ignore_ascii_case("status") && args.is_empty() {
|
||||||
let body = status::build_pipeline_status(dispatch.project_root, dispatch.agents);
|
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));
|
return Some((body, html));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,25 +65,25 @@ fn read_story_blocked(project_root: &std::path::Path, stage_dir: &str, stem: &st
|
|||||||
.unwrap_or(false)
|
.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.
|
/// Priority: blocked > throttled > running > idle.
|
||||||
/// Uses compact Unicode characters (not large emoji) so the output stays
|
/// Uses coloured emoji so indicators render natively in all Matrix clients
|
||||||
/// readable in plain-text chat clients.
|
/// (Element X and others do not support `<font data-mx-color>` HTML tags).
|
||||||
///
|
///
|
||||||
/// - `●` running normally (active agent, no throttle)
|
/// - 🔴 hard-blocked (retry limit exceeded)
|
||||||
/// - `◑` throttled (rate-limit warning received)
|
/// - 🟠 throttled (rate-limit warning received)
|
||||||
/// - `✗` hard-blocked (retry limit exceeded)
|
/// - 🟢 running normally (active agent, no throttle)
|
||||||
/// - `○` idle / no active agent
|
/// - ⚪ idle / no active agent
|
||||||
pub(super) fn traffic_light_dot(blocked: bool, throttled: bool, has_agent: bool) -> &'static str {
|
pub(super) fn traffic_light_dot(blocked: bool, throttled: bool, has_agent: bool) -> &'static str {
|
||||||
if blocked {
|
if blocked {
|
||||||
"\u{2717} " // ✗ — hard blocked
|
"\u{1F534} " // 🔴 — hard blocked
|
||||||
} else if throttled {
|
} else if throttled {
|
||||||
"\u{25D1} " // ◑ — throttled
|
"\u{1F7E0} " // 🟠 — throttled
|
||||||
} else if has_agent {
|
} else if has_agent {
|
||||||
"\u{25CF} " // ● — running normally
|
"\u{1F7E2} " // 🟢 — running normally
|
||||||
} else {
|
} else {
|
||||||
"\u{25CB} " // ○ — idle / no agent
|
"\u{26AA} " // ⚪ — idle / no agent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,34 +122,6 @@ fn read_stage_items(
|
|||||||
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
|
|
||||||
/// `<font data-mx-color="#rrggbb">` 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}', "<font data-mx-color=\"#cc0000\">\u{2717}</font>") // ✗ blocked
|
|
||||||
.replace('\u{25D1}', "<font data-mx-color=\"#ffaa00\">\u{25D1}</font>") // ◑ throttled
|
|
||||||
.replace('\u{25CF}', "<font data-mx-color=\"#00cc00\">\u{25CF}</font>") // ● running
|
|
||||||
.replace('\u{25CB}', "<font data-mx-color=\"#888888\">\u{25CB}</font>") // ○ idle
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the full pipeline status text formatted for Matrix (markdown).
|
/// Build the full pipeline status text formatted for Matrix (markdown).
|
||||||
pub(super) fn build_pipeline_status(project_root: &std::path::Path, agents: &AgentPool) -> String {
|
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.
|
// Build a map from story_id → active AgentInfo for quick lookup.
|
||||||
@@ -557,104 +529,29 @@ mod tests {
|
|||||||
|
|
||||||
// -- traffic_light_dot --------------------------------------------------
|
// -- 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("<font data-mx-color=\"#888888\">\u{25CB}</font>"),
|
|
||||||
"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("<font data-mx-color=\"#cc0000\">\u{2717}</font>"),
|
|
||||||
"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]
|
#[test]
|
||||||
fn dot_idle_when_no_agent() {
|
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]
|
#[test]
|
||||||
fn dot_running_when_agent_not_throttled() {
|
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]
|
#[test]
|
||||||
fn dot_throttled_when_agent_throttled() {
|
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]
|
#[test]
|
||||||
fn dot_blocked_takes_priority_over_throttled() {
|
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]
|
#[test]
|
||||||
fn dot_blocked_when_no_agent_but_blocked_flag() {
|
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 --------------------------------------------------
|
// -- read_story_blocked --------------------------------------------------
|
||||||
@@ -706,8 +603,8 @@ mod tests {
|
|||||||
let output = build_pipeline_status(tmp.path(), &agents);
|
let output = build_pipeline_status(tmp.path(), &agents);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("\u{25CB} "), // ○
|
output.contains("\u{26AA} "), // ⚪
|
||||||
"idle story should show empty-circle dot: {output}"
|
"idle story should show white circle emoji: {output}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,8 +625,8 @@ mod tests {
|
|||||||
let output = build_pipeline_status(tmp.path(), &agents);
|
let output = build_pipeline_status(tmp.path(), &agents);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
output.contains("\u{2717} "), // ✗
|
output.contains("\u{1F534} "), // 🔴
|
||||||
"blocked story should show X dot: {output}"
|
"blocked story should show red circle emoji: {output}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user