From 10003db186adeeccecd767df51edef8e39d927a8 Mon Sep 17 00:00:00 2001 From: Fini Date: Fri, 24 Jul 2026 21:11:54 +0800 Subject: [PATCH] fix(agent): keep status indicator dots out of stub cleanup --- .../op-orchestrator/src/radial_stub_tests.rs | 6 +++ crates/op-orchestrator/src/stub_repair.rs | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/crates/op-orchestrator/src/radial_stub_tests.rs b/crates/op-orchestrator/src/radial_stub_tests.rs index 95b470c9a..63257c1ed 100644 --- a/crates/op-orchestrator/src/radial_stub_tests.rs +++ b/crates/op-orchestrator/src/radial_stub_tests.rs @@ -252,6 +252,8 @@ fn cleanup_deletes_only_empty_decorated_small_frame_stubs() { {"type":"frame","id":"book","name":"Book Btn 1","padding":[8,14],"cornerRadius":9999, "fill":[{"type":"solid","color":"#E5E7EB"}],"children":[]}, {"type":"ellipse","id":"dot","name":"Status Dot","width":6,"height":6,"fill":[{"type":"solid","color":"#22C55E"}]}, + {"type":"frame","id":"active","name":"Active Indicator","width":8,"height":8,"cornerRadius":4, + "fill":[{"type":"solid","color":"#FF6B6B"}],"children":[]}, {"type":"frame","id":"badge","name":"Real Badge","width":58,"height":24,"padding":[5,10],"cornerRadius":9999, "fill":[{"type":"solid","color":"#F97316"}],"children":[{"type":"text","id":"badge-t","content":"-25%"}]}, {"type":"frame","id":"skeleton","name":"Skeleton","width":120,"height":80,"cornerRadius":12, @@ -266,6 +268,10 @@ fn cleanup_deletes_only_empty_decorated_small_frame_stubs() { assert!(find_by_name(&root, "Discount Badge 1").is_none()); assert!(find_by_name(&root, "Book Btn 1").is_none()); assert!(find_by_name(&root, "Status Dot").is_some()); + assert!( + find_by_name(&root, "Active Indicator").is_some(), + "a painted semantic state dot is content, not an abandoned decorated stub" + ); assert!(find_by_name(&root, "Real Badge").is_some()); assert!(find_by_name(&root, "Skeleton").is_some()); assert!(find_by_name(&root, "Spacer").is_some()); diff --git a/crates/op-orchestrator/src/stub_repair.rs b/crates/op-orchestrator/src/stub_repair.rs index 31199e463..a517c4189 100644 --- a/crates/op-orchestrator/src/stub_repair.rs +++ b/crates/op-orchestrator/src/stub_repair.rs @@ -79,6 +79,11 @@ fn is_empty_decorated_stub(v: &Value, resolved_size: Option<(f64, f64)>) -> bool // no child image node, so treating them as empty stubs destroys the // asset before the later avatar-repair pass can inspect it. && !is_meaningful_media_leaf(v) + // A painted state/presence dot is itself the content. Weak models + // often emit it as a tiny childless frame rather than an ellipse; once + // the radius pass makes it circular it must not be mistaken for an + // abandoned badge shell. + && !has_state_indicator_semantics(v) && has_visible_paint(v) && (padding_positive(v) || numeric(v, "cornerRadius").is_some_and(|r| r > 0.0)) && width.is_some_and(|w| w > 0.0 && w < 80.0) @@ -89,6 +94,47 @@ fn is_meaningful_media_leaf(v: &Value) -> bool { has_image_fill(v) || has_avatar_semantics(v) } +fn has_state_indicator_semantics(v: &Value) -> bool { + let role = v + .get("role") + .and_then(Value::as_str) + .unwrap_or_default() + .to_ascii_lowercase(); + if matches!( + role.as_str(), + "status-dot" + | "status-indicator" + | "presence-dot" + | "presence-indicator" + | "online-indicator" + | "notification-dot" + ) { + return true; + } + + let name = v + .get("name") + .and_then(Value::as_str) + .unwrap_or_default() + .to_ascii_lowercase(); + [ + "status dot", + "status indicator", + "active indicator", + "presence dot", + "presence indicator", + "online dot", + "online indicator", + "notification dot", + "状态点", + "状态指示", + "在线点", + "在线指示", + ] + .iter() + .any(|needle| name.contains(needle)) +} + fn has_image_fill(v: &Value) -> bool { match v.get("fill") { Some(Value::Array(fills)) => fills.iter().any(is_image_paint),