fix(tui): refresh prompt on mode changes

Refs #2379

Harvested from PR #2534 by @cyq1017
This commit is contained in:
Hunter Bown
2026-06-01 16:14:59 -07:00
parent 46de1a9b2d
commit bc7f98a6a0
6 changed files with 125 additions and 18 deletions
+25 -5
View File
@@ -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);
}
+2
View File
@@ -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:?}")))
+43
View File
@@ -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::<Vec<_>>()
.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"}}"#;
+2
View File
@@ -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.
+37 -13
View File
@@ -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 } => {
+16
View File
@@ -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();