diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index 38e7a2f8a..ebe6152b8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -150,14 +150,26 @@ fn persisted_design_reasoning_entries(session: &DesignSession) -> Vec = Vec::new(); + let mut pending_reasoning: Vec = Vec::new(); let mut saw_response_output = false; for item in &session.history { if item.get("role").and_then(Value::as_str) == Some("user") { if !pending_reasoning.is_empty() || !current_reasoning.is_empty() { pending_reasoning.append(&mut current_reasoning); + // A user item closes the previous turn. Resolve its reasoning + // against that turn's last assistant message before moving to + // the next group; otherwise it is incorrectly attached to the + // next turn and rendered at the bottom as an orphan. + let assistant_id = assistant_groups + .get(group_index) + .and_then(|ids| ids.last()) + .cloned(); + for mut entry in pending_reasoning.drain(..) { + entry.message_id = assistant_id.clone(); + entries.push(entry); + } } group_index += 1; assistant_index = 0; @@ -1638,6 +1650,53 @@ mod tests { ); } + #[test] + fn persisted_reasoning_stays_with_the_turn_before_an_approval_boundary() { + let mut session = new_design_session("project", "quality"); + session.messages = vec![ + DesignMessage { + id: "turn-1:user".into(), + role: "user".into(), + text: "第一轮需求".into(), + }, + DesignMessage { + id: "turn-1:assistant".into(), + role: "assistant".into(), + text: "第一轮已提交审批".into(), + }, + DesignMessage { + id: "turn-2:user".into(), + role: "user".into(), + text: "用户已批准,进入下一阶段".into(), + }, + DesignMessage { + id: "turn-2:assistant".into(), + role: "assistant".into(), + text: "查询工作阶段".into(), + }, + ]; + session.history = vec![ + json!({"role":"user", "content":"第一轮需求"}), + json!({"type":"reasoning", "id":"before-approval", "content":[{"type":"reasoning_text", "text":"审批前的思考"}]}), + json!({"type":"message", "role":"assistant", "content":[{"type":"output_text", "text":"第一轮已提交审批"}]}), + json!({"role":"user", "content":"用户已批准,进入下一阶段"}), + json!({"type":"reasoning", "id":"after-approval", "content":[{"type":"reasoning_text", "text":"审批后的思考"}]}), + json!({"type":"message", "role":"assistant", "content":[{"type":"output_text", "text":"查询工作阶段"}]}), + ]; + + let entries = persisted_design_reasoning_entries(&session); + assert_eq!( + entries + .iter() + .map(|entry| (entry.id.as_str(), entry.message_id.as_deref())) + .collect::>(), + vec![ + ("before-approval", Some("turn-1:assistant")), + ("after-approval", Some("turn-2:assistant")), + ] + ); + } + #[tokio::test(flavor = "current_thread")] async fn scripted_design_provider_emits_reasoning_without_persisting_it() { let (_temp, root, _resources) = init_design_project();