fix(ai): stop a stray selection hijacking a new-design prompt

A section-heavy new-design prompt ('Design a … page. Include a search
section …') trips is_section_add_request, so requests_new_whole_screen
returns false; the selection-modify bias then routed the whole prompt
into run_modify_turn, where M3's flat-JSONL output was renest-dropped
to nothing ('Could not parse design nodes'). Add a section-add-blind
creation-signal veto to both routing gates so a new-design request
reaches the design pipeline regardless of an active selection.
This commit is contained in:
Fini 2026-07-04 16:26:13 +08:00
parent 0dd7277b78
commit 9de2249164
3 changed files with 52 additions and 2 deletions

View file

@ -240,6 +240,13 @@ fn should_launch_direct_modify(state: &EditorState, user_text: &str) -> bool {
if op_host_services::chat_intent::requests_new_whole_screen(user_text) {
return false;
}
// Same intent, but section-add-blind: a full new-page spec that mentions
// "section" ("Include a search section…") still reads as new, so a stray
// selection can't drag it into modify (measured: a travel-app design with
// a node selected fell into run_modify_turn → M3 flat-JSONL → empty).
if op_host_services::chat_intent::has_new_screen_creation_signal(user_text) {
return false;
}
let keyword_intent = op_host_services::chat_intent::classify_by_keywords(user_text);
let selected_target_instruction = !state.selection.set.is_empty()
&& keyword_intent != op_host_services::chat_intent::DesignIntent::Chat;

View file

@ -180,8 +180,13 @@ pub fn classify_intent_for_standard_route(
if !state.selection.set.is_empty() && keyword_intent == DesignIntent::Chat {
return DesignIntent::Chat;
}
let selected_target_instruction =
!state.selection.set.is_empty() && keyword_intent != DesignIntent::Chat;
// A selection biases toward modify — UNLESS the prompt is itself a
// whole-screen design request (a stray selection must not hijack "Design
// a … page" into an edit; `requests_new_whole_screen` misses these when
// they mention "section", so check the veto-free creation signal too).
let selected_target_instruction = !state.selection.set.is_empty()
&& keyword_intent != DesignIntent::Chat
&& !has_new_screen_creation_signal(text);
if keyword_intent == DesignIntent::Modify || selected_target_instruction {
return DesignIntent::Modify;
}
@ -468,6 +473,23 @@ pub fn requests_new_whole_screen(prompt: &str) -> bool {
english_requests_new_screen(&prompt.to_lowercase())
}
/// Core "this is a NEW screen/design" signal: a creation verb aimed at a
/// page/screen noun. Unlike [`requests_new_whole_screen`] it does NOT veto on
/// section-add phrasing — a full new-page spec that merely mentions the word
/// "section" ("Include a search section…", "Deals of the Week section") still
/// reads as new. Used to stop a stray canvas selection from hijacking a
/// whole-screen design prompt into the modify path (measured: a travel-app
/// design with a node selected routed to modify → M3 flat-JSONL → empty).
pub fn has_new_screen_creation_signal(prompt: &str) -> bool {
let cjk_page = ["页面", "页", "屏幕", "屏"]
.iter()
.any(|k| prompt.contains(k));
if cjk_page && DRAW_VERB_CJK.iter().any(|v| prompt.contains(v)) {
return true;
}
english_requests_new_screen(&prompt.to_lowercase())
}
/// English new-screen detection by the DETERMINER nearest a page/screen noun:
/// an indefinite article ("a / an / another / new") means a NEW page is being
/// made ("make a login page", "draw a checkout page"); a definite/demonstrative

View file

@ -82,3 +82,24 @@ fn selection_bias_does_not_hijack_whole_new_screen_or_chat() {
DesignIntent::Chat
);
}
#[test]
fn selection_does_not_hijack_english_section_heavy_new_design() {
// Regression: "Design a … page" whose spec mentions "section" three times
// trips `is_section_add_request`, so `requests_new_whole_screen` is false;
// a stray selection then dragged the whole new-design prompt into modify
// (measured: M3 flat-JSONL → "Could not parse design nodes"). The
// creation-signal veto keeps it New.
let provider = Scripted;
let state = state_with_selected_card();
let prompt = "Design a travel booking mobile app explore page. Include a search section with \"Where to?\" input, date picker chips, and guest count. \"Deals of the Week\" section with 2 featured deal cards. Recently viewed section with 2 compact cards. Bottom tab bar. Warm, inviting design with orange accents.";
assert!(
crate::chat_intent::has_new_screen_creation_signal(prompt),
"creation signal must fire on a design-a-page prompt"
);
assert_ne!(
classify_intent_for_standard_route(&provider, &state, prompt, None),
DesignIntent::Modify,
"a section-heavy new-design prompt must not be hijacked to modify by a selection"
);
}