fix(agent): keep status indicator dots out of stub cleanup

This commit is contained in:
Fini 2026-07-24 21:11:54 +08:00
parent 239ea6f10d
commit 10003db186
2 changed files with 52 additions and 0 deletions

View file

@ -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());

View file

@ -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),