fix(tui): refresh prompt on mode changes
Refs #2379 Harvested from PR #2534 by @cyq1017
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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:?}")))
|
||||
|
||||
@@ -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"}}"#;
|
||||
|
||||
@@ -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
@@ -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 } => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user