fix(orchestrator): append a bottom-nav subtask to app home plans

Multi-section mobile home/feed/dashboard plans that omit the tab bar
ship looking amputated; single-task flows stay untouched.
This commit is contained in:
Fini 2026-07-04 02:15:57 +08:00
parent dbf6bc0d1d
commit a9845e6239

View file

@ -89,7 +89,17 @@ fn is_bottom_nav_subtask(st: &Subtask) -> bool {
}
fn ensure_requested_bottom_nav_subtask(plan: &mut OrchestratorPlan, req: &DesignRequest) {
if !prompt_requests_bottom_nav(&req.prompt) || plan.subtasks.iter().any(is_bottom_nav_subtask) {
if plan.subtasks.iter().any(is_bottom_nav_subtask) {
return;
}
// Two ways in: the prompt asked for it, OR this is an app HOME/main
// screen — a multi-section mobile plan whose root/screen reads as a
// home/feed — where a bottom tab bar is anatomy, not an option (a
// glm "Food App Home" planned 4 content sections and simply skipped
// the navbar the teaching asked for; plan-level completeness is the
// deterministic backstop). Single-task flows (<3 sections) never
// qualify.
if !prompt_requests_bottom_nav(&req.prompt) && !plan_is_app_home_screen(plan) {
return;
}
@ -112,6 +122,25 @@ fn ensure_requested_bottom_nav_subtask(plan: &mut OrchestratorPlan, req: &Design
});
}
/// A multi-section mobile plan whose root frame is named like an app
/// home/main/feed screen — the shape whose bottom tab bar is mandatory.
fn plan_is_app_home_screen(plan: &OrchestratorPlan) -> bool {
if plan.subtasks.len() < 3 {
return false;
}
let name = plan.root_frame.name.to_lowercase();
[
"home",
"main screen",
"feed",
"discover",
"browse",
"dashboard",
]
.iter()
.any(|k| name.contains(k))
}
/// 就地规范化 `plan`:
/// - 一次性判定 `is_mobile`(根 frame 宽度);
/// - 移动端剔除 plan 自带的状态栏 subtask(状态栏改由 scaffold 注入);
@ -230,6 +259,49 @@ mod tests {
}
}
#[test]
fn app_home_plan_without_navbar_gets_one_appended() {
// glm planned "Food App Home" with 4 content sections and no navbar —
// an app home's bottom tab bar is anatomy, not an option.
let mut p = plan(
390.0,
vec![
subtask("header", "Header & Search"),
subtask("cats", "Category Rail"),
subtask("feat", "Featured Restaurant Banner"),
subtask("popular", "Popular Dishes"),
],
);
p.root_frame.name = "Food App Home".into();
normalize(&mut p, &req_with_prompt("well-designed food app"));
let labels: Vec<String> = p.subtasks.iter().map(|s| s.label.to_lowercase()).collect();
assert!(
labels.iter().any(|l| l.contains("navigation")),
"navbar appended: {labels:?}"
);
assert!(
labels.last().unwrap().contains("navigation"),
"navbar is LAST: {labels:?}"
);
}
#[test]
fn single_task_mobile_flow_gets_no_navbar() {
let mut p = plan(
390.0,
vec![subtask("form", "Login Form"), subtask("cta", "Actions")],
);
p.root_frame.name = "Login Screen".into();
normalize(&mut p, &req_with_prompt("mobile login screen"));
assert!(
!p.subtasks
.iter()
.any(|s| s.label.to_lowercase().contains("navigation")),
"{:?}",
p.subtasks.iter().map(|s| &s.label).collect::<Vec<_>>()
);
}
#[test]
fn normalize_assigns_id_prefix_and_parent() {
let mut p = plan(