diff --git a/crates/tui/src/commands/config.rs b/crates/tui/src/commands/config.rs index fad755e1..24d4e491 100644 --- a/crates/tui/src/commands/config.rs +++ b/crates/tui/src/commands/config.rs @@ -725,16 +725,33 @@ pub fn mode(app: &mut App, arg: Option<&str>) -> CommandResult { return CommandResult::action(AppAction::OpenModePicker); }; match parse_mode_arg(arg) { - Some(mode) => CommandResult::message(switch_mode(app, mode)), + Some(mode) => { + let (message, changed) = switch_mode_with_status(app, mode); + if changed { + CommandResult::with_message_and_action(message, AppAction::ModeChanged(mode)) + } else { + CommandResult::message(message) + } + } None => CommandResult::error("Usage: /mode [agent|plan|yolo|1|2|3]"), } } pub fn switch_mode(app: &mut App, mode: AppMode) -> String { + switch_mode_with_status(app, mode).0 +} + +fn switch_mode_with_status(app: &mut App, mode: AppMode) -> (String, bool) { if app.set_mode(mode) { - format!("Switched to {} mode.", mode_display_name(mode)) + ( + format!("Switched to {} mode.", mode_display_name(mode)), + true, + ) } else { - format!("Already in {} mode.", mode_display_name(mode)) + ( + format!("Already in {} mode.", mode_display_name(mode)), + false, + ) } } @@ -1499,6 +1516,7 @@ mod tests { let _ = mode(&mut app, Some("agent")); let result = mode(&mut app, Some("yolo")); assert!(result.message.unwrap().contains("Switched to YOLO mode")); + assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Yolo))); assert!(app.allow_shell); assert!(app.trust_mode); assert!(app.yolo); @@ -1511,9 +1529,11 @@ mod tests { let mut app = create_test_app(); let _ = mode(&mut app, Some("agent")); assert_eq!(app.mode, AppMode::Agent); - let _ = mode(&mut app, Some("2")); + let result = mode(&mut app, Some("2")); + assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Plan))); assert_eq!(app.mode, AppMode::Plan); - let _ = mode(&mut app, Some("3")); + let result = mode(&mut app, Some("3")); + assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Yolo))); assert_eq!(app.mode, AppMode::Yolo); } diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index e5e4c612..9483315a 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -784,6 +784,8 @@ impl Engine { let _ = self.tx_event.send(Event::AgentList { agents }).await; } Op::ChangeMode { mode } => { + self.refresh_system_prompt(mode); + self.emit_session_updated().await; let _ = self .tx_event .send(Event::status(format!("Mode changed to: {mode:?}"))) diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index d12a2af4..ecd1d239 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -1293,6 +1293,49 @@ async fn set_model_reloads_instruction_sources_and_updates_session_prompt() { assert!(!prompt.contains("FLASH_INSTRUCTIONS_MARKER")); } +#[tokio::test] +async fn change_mode_refreshes_session_prompt_and_updates_session() { + let tmp = tempdir().expect("tempdir"); + let config = EngineConfig { + workspace: tmp.path().to_path_buf(), + model: "deepseek-v4-pro".to_string(), + ..Default::default() + }; + let (engine, handle) = Engine::new(config, &Config::default()); + + let run = tokio::spawn(engine.run()); + handle + .send(Op::ChangeMode { + mode: AppMode::Yolo, + }) + .await + .expect("send change mode"); + + let prompt = { + let mut rx = handle.rx_event.write().await; + loop { + let event = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv()) + .await + .expect("session update after mode switch") + .expect("event"); + if let Event::SessionUpdated { system_prompt, .. } = event { + break match system_prompt.expect("system prompt") { + SystemPrompt::Text(text) => text, + SystemPrompt::Blocks(blocks) => blocks + .into_iter() + .map(|block| block.text) + .collect::>() + .join("\n"), + }; + } + } + }; + run.abort(); + + assert!(prompt.contains("Mode: YOLO")); + assert!(prompt.contains("Approval Policy: Auto")); +} + #[test] fn detects_context_length_errors_from_provider_payloads() { let msg = r#"SSE stream request failed: HTTP 400 Bad Request: {"error":{"message":"This model's maximum context length is 131072 tokens. However, you requested 153056 tokens (148960 in the messages, 4096 in the completion).","type":"invalid_request_error"}}"#; diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index e0d2eb4b..d907e7d7 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -4803,6 +4803,8 @@ pub enum AppAction { OpenProviderPicker, /// Open the `/mode` picker modal for Agent / Plan / YOLO. OpenModePicker, + /// Refresh the engine prompt after the UI operating mode changes. + ModeChanged(AppMode), /// Open the `/statusline` multi-select picker for footer items. OpenStatusPicker, /// Open the `/feedback` picker for GitHub issue/security destinations. diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 50c4b0b0..3421e65e 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -3132,7 +3132,7 @@ async fn run_event_loop( app.set_sidebar_focus(SidebarFocus::Work); app.status_message = Some("Sidebar focus: work".to_string()); } else { - app.set_mode(AppMode::Plan); + apply_mode_update(app, &engine_handle, AppMode::Plan).await; } continue; } @@ -3141,7 +3141,7 @@ async fn run_event_loop( app.set_sidebar_focus(SidebarFocus::Tasks); app.status_message = Some("Sidebar focus: tasks".to_string()); } else { - app.set_mode(AppMode::Agent); + apply_mode_update(app, &engine_handle, AppMode::Agent).await; } continue; } @@ -3150,7 +3150,7 @@ async fn run_event_loop( app.set_sidebar_focus(SidebarFocus::Agents); app.status_message = Some("Sidebar focus: agents".to_string()); } else { - app.set_mode(AppMode::Yolo); + apply_mode_update(app, &engine_handle, AppMode::Yolo).await; } continue; } @@ -3432,7 +3432,11 @@ async fn run_event_loop( continue; } let prior_model = app.model.clone(); + let prior_mode = app.mode; app.cycle_mode(); + if app.mode != prior_mode { + sync_mode_update(&engine_handle, app.mode).await; + } if app.model != prior_model { let _ = engine_handle .send(Op::SetModel { @@ -3899,34 +3903,34 @@ async fn run_event_loop( AppMode::Agent => AppMode::Yolo, AppMode::Yolo => AppMode::Plan, }; - app.set_mode(new_mode); + apply_mode_update(app, &engine_handle, new_mode).await; } } _ if key_shortcuts::is_paste_shortcut(&key) => { app.paste_from_clipboard(); } KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::ALT) => { - app.set_mode(AppMode::Agent); + apply_mode_update(app, &engine_handle, AppMode::Agent).await; continue; } KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::ALT) => { - app.set_mode(AppMode::Yolo); + apply_mode_update(app, &engine_handle, AppMode::Yolo).await; continue; } KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::ALT) => { - app.set_mode(AppMode::Plan); + apply_mode_update(app, &engine_handle, AppMode::Plan).await; continue; } KeyCode::Char('A') if key.modifiers.contains(KeyModifiers::ALT) => { - app.set_mode(AppMode::Agent); + apply_mode_update(app, &engine_handle, AppMode::Agent).await; continue; } KeyCode::Char('Y') if key.modifiers.contains(KeyModifiers::ALT) => { - app.set_mode(AppMode::Yolo); + apply_mode_update(app, &engine_handle, AppMode::Yolo).await; continue; } KeyCode::Char('P') if key.modifiers.contains(KeyModifiers::ALT) => { - app.set_mode(AppMode::Plan); + apply_mode_update(app, &engine_handle, AppMode::Plan).await; continue; } KeyCode::Char('v') | KeyCode::Char('V') @@ -4746,6 +4750,19 @@ async fn dispatch_user_message( Ok(()) } +async fn sync_mode_update(engine_handle: &EngineHandle, mode: AppMode) { + let _ = engine_handle.send(Op::ChangeMode { mode }).await; +} + +async fn apply_mode_update(app: &mut App, engine_handle: &EngineHandle, mode: AppMode) -> bool { + if app.set_mode(mode) { + sync_mode_update(engine_handle, mode).await; + true + } else { + false + } +} + async fn apply_model_and_compaction_update( engine_handle: &EngineHandle, compaction: crate::compaction::CompactionConfig, @@ -5137,6 +5154,9 @@ async fn apply_command_result( persistence_actor::persist(PersistRequest::ClearCheckpoint); } } + AppAction::ModeChanged(mode) => { + sync_mode_update(engine_handle, mode).await; + } AppAction::SendMessage(content) => { let queued = build_queued_message(app, content); submit_or_steer_message(app, config, engine_handle, queued).await?; @@ -6014,7 +6034,7 @@ async fn apply_plan_choice( ) -> Result<()> { match choice { PlanChoice::AcceptAgent => { - app.set_mode(AppMode::Agent); + apply_mode_update(app, engine_handle, AppMode::Agent).await; app.add_message(HistoryCell::System { content: "Plan accepted. Switching to Agent mode and starting implementation." .to_string(), @@ -6029,7 +6049,7 @@ async fn apply_plan_choice( } } PlanChoice::AcceptYolo => { - app.set_mode(AppMode::Yolo); + apply_mode_update(app, engine_handle, AppMode::Yolo).await; app.add_message(HistoryCell::System { content: "Plan accepted. Switching to YOLO mode and starting implementation." .to_string(), @@ -6050,7 +6070,7 @@ async fn apply_plan_choice( app.status_message = Some("Revise the plan and press Enter.".to_string()); } PlanChoice::ExitPlan => { - app.set_mode(AppMode::Agent); + apply_mode_update(app, engine_handle, AppMode::Agent).await; app.add_message(HistoryCell::System { content: "Exited Plan mode. Switched to Agent mode.".to_string(), }); @@ -6840,7 +6860,11 @@ async fn handle_view_events( .await; } ViewEvent::ModeSelected { mode } => { + let prior_mode = app.mode; let msg = commands::switch_mode(app, mode); + if app.mode != prior_mode { + sync_mode_update(engine_handle, app.mode).await; + } app.add_message(HistoryCell::System { content: msg }); } ViewEvent::BacktrackStep { direction } => { diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index a03c9258..6ea4de95 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -2039,6 +2039,22 @@ async fn model_change_update_syncs_engine_model_before_compaction() { } } +#[tokio::test] +async fn mode_change_update_notifies_engine() { + let mut app = create_test_app(); + let _ = app.set_mode(crate::tui::app::AppMode::Plan); + let mut engine = crate::core::engine::mock_engine_handle(); + + assert!(apply_mode_update(&mut app, &engine.handle, crate::tui::app::AppMode::Yolo).await); + + match engine.rx_op.recv().await.expect("change mode op") { + crate::core::ops::Op::ChangeMode { mode } => { + assert_eq!(mode, crate::tui::app::AppMode::Yolo); + } + other => panic!("expected ChangeMode, got {other:?}"), + } +} + #[test] fn saved_default_provider_syncs_back_to_runtime_config() { let _home = SettingsHomeGuard::new();