From a9845e62397ed9bfa0e6dfe663687850bcc736b4 Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 4 Jul 2026 02:15:57 +0800 Subject: [PATCH] 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. --- crates/op-orchestrator/src/plan_normalize.rs | 74 +++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/crates/op-orchestrator/src/plan_normalize.rs b/crates/op-orchestrator/src/plan_normalize.rs index b5b3b3e8e..3e503e13f 100644 --- a/crates/op-orchestrator/src/plan_normalize.rs +++ b/crates/op-orchestrator/src/plan_normalize.rs @@ -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 = 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::>() + ); + } + #[test] fn normalize_assigns_id_prefix_and_parent() { let mut p = plan(