diff --git a/apps/native/src-tauri/src/evolve/lifecycle.rs b/apps/native/src-tauri/src/evolve/lifecycle.rs index c5422678e..f0d02cdfc 100644 --- a/apps/native/src-tauri/src/evolve/lifecycle.rs +++ b/apps/native/src-tauri/src/evolve/lifecycle.rs @@ -231,7 +231,15 @@ pub async fn backup_evolve_and_record_changeset( // Short-circuit: conversational responses made no environment changes. if evolution.state == EvolutionState::Conversational { info!("[evolution] Conversational response — skipping git/db workflow"); - let evolve_state = evolve_state::get(app).unwrap_or_default(); + let evolve_state = evolve_state::set( + app, + EvolveState { + last_evolution_state: Some(EvolutionState::Conversational), + ..evolve_state::get(app).unwrap_or_default() + }, + &initial_status.changes, + ) + .unwrap_or_else(|_| evolve_state::get(app).unwrap_or_default()); return Ok(EvolutionResult { change_map: SemanticChangeMap::default(), git_status: initial_status, @@ -271,6 +279,7 @@ pub async fn backup_evolve_and_record_changeset( evolution_id: db_evolution_id, current_changeset_id: new_changeset_id, backup_branch, + last_evolution_state: Some(evolution.state.clone()), ..current_state }, &final_status.changes, @@ -321,6 +330,7 @@ fn restore_after_failure(app: &AppHandle, config_dir: &str, backup_branch: &Opti app, EvolveState { backup_branch: None, + last_evolution_state: Some(EvolutionState::Failed), ..evolve_state::get(app).unwrap_or_default() }, &[], diff --git a/apps/native/src-tauri/src/managed_edits/managed_edit.rs b/apps/native/src-tauri/src/managed_edits/managed_edit.rs index 40fcacf73..059b00109 100644 --- a/apps/native/src-tauri/src/managed_edits/managed_edit.rs +++ b/apps/native/src-tauri/src/managed_edits/managed_edit.rs @@ -63,6 +63,7 @@ pub fn prepare_managed_edit(app: &AppHandle) -> Result { rollback_store_path, rollback_changeset_id, step: shared_types::EvolveStep::Evolve, + last_evolution_state: None, }, &pre_edit_status.changes, ) diff --git a/apps/native/src-tauri/src/shared_types/evolve.rs b/apps/native/src-tauri/src/shared_types/evolve.rs index d5fc28585..f025af3c2 100644 --- a/apps/native/src-tauri/src/shared_types/evolve.rs +++ b/apps/native/src-tauri/src/shared_types/evolve.rs @@ -101,6 +101,12 @@ pub struct EvolveState { pub rollback_changeset_id: Option, /// UI step derived from the routing state. pub step: EvolveStep, + /// Last terminal state observed for this routing session. + /// + /// This supports transition-sensitive behavior when returning to Begin + /// and maybe some other useful things in the future. + #[serde(default)] + pub last_evolution_state: Option, } impl Default for EvolveState { @@ -115,6 +121,7 @@ impl Default for EvolveState { rollback_store_path: None, rollback_changeset_id: None, step: EvolveStep::Begin, + last_evolution_state: None, } } } diff --git a/apps/native/src-tauri/src/state/evolve_state.rs b/apps/native/src-tauri/src/state/evolve_state.rs index b1d632b2b..5192ef10e 100644 --- a/apps/native/src-tauri/src/state/evolve_state.rs +++ b/apps/native/src-tauri/src/state/evolve_state.rs @@ -5,7 +5,7 @@ use tauri::{AppHandle, Runtime}; use tauri_plugin_store::StoreExt; use crate::evolve::session_chat_memory_store; -use crate::shared_types::{EvolveState, EvolveStep}; +use crate::shared_types::{EvolutionState, EvolveState, EvolveStep}; use crate::sqlite_types::Change; impl EvolveState { @@ -24,8 +24,12 @@ impl EvolveState { const EVOLVE_STATE_PATH: &str = "evolve-state.json"; const EVOLVE_STATE_KEY: &str = "evolveState"; -fn clear_chat_memory_if_begin(step: &EvolveStep, clear_fn: impl FnOnce()) { - if *step == EvolveStep::Begin { +fn clear_chat_memory_if_begin( + step: &EvolveStep, + preserve_for_conversational_begin: bool, + clear_fn: impl FnOnce(), +) { + if *step == EvolveStep::Begin && !preserve_for_conversational_begin { clear_fn(); } } @@ -52,6 +56,16 @@ pub fn set( let is_built = crate::state::build_state::current_state_built(app, current_changes); let has_changes = !current_changes.is_empty(); state.recompute_step(is_built, has_changes); + + // Preserve chat memory whenever we are at Begin and the last evolution + // outcome was conversational (no edits). This intentionally persists + // across repeated conversational cycles. + let preserve_for_conversational_begin = state.step == EvolveStep::Begin + && matches!( + state.last_evolution_state.as_ref(), + Some(EvolutionState::Conversational) + ); + let store = app.store(EVOLVE_STATE_PATH)?; store.set(EVOLVE_STATE_KEY, serde_json::to_value(&state)?); store.save()?; @@ -62,7 +76,9 @@ pub fn set( // Note that `clear` is NOT a suitable place to do this since it is not called // on all possible transitions back to Begin (e.g. Evolve -> Begin when evolution_id // is cleared but committable is still true). - clear_chat_memory_if_begin(&state.step, || session_chat_memory_store().clear()); + clear_chat_memory_if_begin(&state.step, preserve_for_conversational_begin, || { + session_chat_memory_store().clear() + }); Ok(state) } @@ -79,7 +95,7 @@ mod tests { #[test] fn clear_chat_memory_if_begin_calls_clear() { let mut cleared = false; - clear_chat_memory_if_begin(&EvolveStep::Begin, || { + clear_chat_memory_if_begin(&EvolveStep::Begin, false, || { cleared = true; }); assert!(cleared); @@ -88,17 +104,35 @@ mod tests { #[test] fn clear_chat_memory_if_begin_skips_non_begin_steps() { let mut cleared = false; - clear_chat_memory_if_begin(&EvolveStep::Evolve, || { + clear_chat_memory_if_begin(&EvolveStep::Evolve, false, || { + cleared = true; + }); + assert!(!cleared); + + clear_chat_memory_if_begin(&EvolveStep::Commit, false, || { cleared = true; }); assert!(!cleared); + } - clear_chat_memory_if_begin(&EvolveStep::Commit, || { + #[test] + fn clear_chat_memory_if_begin_skips_when_last_state_is_conversational() { + let mut cleared = false; + clear_chat_memory_if_begin(&EvolveStep::Begin, true, || { cleared = true; }); assert!(!cleared); } + #[test] + fn clear_chat_memory_if_begin_clears_for_non_conversational_last_state() { + let mut cleared = false; + clear_chat_memory_if_begin(&EvolveStep::Begin, false, || { + cleared = true; + }); + assert!(cleared); + } + #[test] fn recomputed_begin_triggers_clear_logic() { let mut state = EvolveState { @@ -109,7 +143,7 @@ mod tests { state.recompute_step(true, false); let mut cleared = false; - clear_chat_memory_if_begin(&state.step, || { + clear_chat_memory_if_begin(&state.step, false, || { cleared = true; });