diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 8a41dc60e88eb525a4dd6b75d5a15a1fd2db7787..58a67d2578dd50cf72151a12af6cc3510dbe1410 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -166,6 +166,7 @@ impl Render for PromptEditor { .child( h_flex() .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::secondary_confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_down)) @@ -530,6 +531,20 @@ impl PromptEditor { } fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { + self.handle_confirm(false, cx); + } + + fn secondary_confirm( + &mut self, + _: &menu::SecondaryConfirm, + _window: &mut Window, + cx: &mut Context, + ) { + let execute = matches!(self.mode, PromptEditorMode::Terminal { .. }); + self.handle_confirm(execute, cx); + } + + fn handle_confirm(&mut self, execute: bool, cx: &mut Context) { match self.codegen_status(cx) { CodegenStatus::Idle => { self.fire_started_telemetry(cx); @@ -541,7 +556,7 @@ impl PromptEditor { self.fire_started_telemetry(cx); cx.emit(PromptEditorEvent::StartRequested); } else { - cx.emit(PromptEditorEvent::ConfirmRequested { execute: false }); + cx.emit(PromptEditorEvent::ConfirmRequested { execute }); } } CodegenStatus::Error(_) => { @@ -1637,3 +1652,205 @@ fn insert_message_creases( editor.fold_creases(creases, false, window, cx); ids } + +#[cfg(test)] +mod tests { + use super::*; + use crate::terminal_codegen::TerminalCodegen; + use agent::ThreadStore; + use collections::VecDeque; + use fs::FakeFs; + use gpui::{TestAppContext, VisualTestContext}; + use language::Buffer; + use project::Project; + use settings::SettingsStore; + use std::cell::RefCell; + use std::path::Path; + use std::rc::Rc; + use terminal::TerminalBuilder; + use terminal::terminal_settings::CursorShape; + use util::path; + use util::paths::PathStyle; + use uuid::Uuid; + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + theme_settings::init(theme::LoadThemes::JustBase, cx); + editor::init(cx); + release_channel::init(semver::Version::new(0, 0, 0), cx); + language_model::LanguageModelRegistry::test(cx); + prompt_store::init(cx); + }); + } + + fn build_terminal_prompt_editor( + workspace: &Entity, + cx: &mut VisualTestContext, + ) -> Entity> { + let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx))); + let fs = FakeFs::new(cx.executor()); + + let terminal = cx.update(|_window, cx| { + cx.new(|cx| { + TerminalBuilder::new_display_only( + CursorShape::default(), + settings::AlternateScroll::On, + None, + 0, + cx.background_executor(), + PathStyle::local(), + ) + .unwrap() + .subscribe(cx) + }) + }); + + let session_id = Uuid::new_v4(); + let codegen = + cx.update(|_window, cx| cx.new(|_| TerminalCodegen::new(terminal, session_id))); + + let prompt_buffer = cx.update(|_window, cx| { + cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local("", cx)), cx)) + }); + + let project = workspace.update(cx, |workspace, _cx| workspace.project().downgrade()); + + cx.update(|window, cx| { + cx.new(|cx| { + PromptEditor::new_terminal( + TerminalInlineAssistId::default(), + VecDeque::new(), + prompt_buffer, + codegen, + session_id, + fs, + thread_store, + None, + project, + workspace.downgrade(), + window, + cx, + ) + }) + }) + } + + #[gpui::test] + async fn test_secondary_confirm_emits_execute_true_in_terminal_mode(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/project", serde_json::json!({"file": ""})) + .await; + let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let prompt_editor = build_terminal_prompt_editor(&workspace, cx); + + // Set the codegen status to Done so that confirm logic emits ConfirmRequested. + prompt_editor.update(cx, |editor, cx| { + editor.codegen().update(cx, |codegen, _| { + codegen.status = CodegenStatus::Done; + }); + editor.edited_since_done = false; + }); + + let events: Rc>> = Rc::new(RefCell::new(Vec::new())); + let events_clone = events.clone(); + cx.update(|_window, cx| { + cx.subscribe(&prompt_editor, move |_, event: &PromptEditorEvent, _cx| { + events_clone.borrow_mut().push(match event { + PromptEditorEvent::ConfirmRequested { execute } => { + PromptEditorEvent::ConfirmRequested { execute: *execute } + } + PromptEditorEvent::StartRequested => PromptEditorEvent::StartRequested, + PromptEditorEvent::StopRequested => PromptEditorEvent::StopRequested, + PromptEditorEvent::CancelRequested => PromptEditorEvent::CancelRequested, + PromptEditorEvent::Resized { height_in_lines } => PromptEditorEvent::Resized { + height_in_lines: *height_in_lines, + }, + }); + }) + .detach(); + }); + + // Dispatch menu::SecondaryConfirm (cmd-enter). + prompt_editor.update(cx, |editor, cx| { + editor.handle_confirm(true, cx); + }); + + let events = events.borrow(); + assert_eq!(events.len(), 1, "Expected exactly one event"); + assert!( + matches!( + events[0], + PromptEditorEvent::ConfirmRequested { execute: true } + ), + "Expected ConfirmRequested with execute: true, got {:?}", + match &events[0] { + PromptEditorEvent::ConfirmRequested { execute } => + format!("ConfirmRequested {{ execute: {} }}", execute), + _ => "other event".to_string(), + } + ); + } + + #[gpui::test] + async fn test_confirm_emits_execute_false_in_terminal_mode(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/project", serde_json::json!({"file": ""})) + .await; + let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let prompt_editor = build_terminal_prompt_editor(&workspace, cx); + + prompt_editor.update(cx, |editor, cx| { + editor.codegen().update(cx, |codegen, _| { + codegen.status = CodegenStatus::Done; + }); + editor.edited_since_done = false; + }); + + let events: Rc>> = Rc::new(RefCell::new(Vec::new())); + let events_clone = events.clone(); + cx.update(|_window, cx| { + cx.subscribe(&prompt_editor, move |_, event: &PromptEditorEvent, _cx| { + events_clone.borrow_mut().push(match event { + PromptEditorEvent::ConfirmRequested { execute } => { + PromptEditorEvent::ConfirmRequested { execute: *execute } + } + PromptEditorEvent::StartRequested => PromptEditorEvent::StartRequested, + PromptEditorEvent::StopRequested => PromptEditorEvent::StopRequested, + PromptEditorEvent::CancelRequested => PromptEditorEvent::CancelRequested, + PromptEditorEvent::Resized { height_in_lines } => PromptEditorEvent::Resized { + height_in_lines: *height_in_lines, + }, + }); + }) + .detach(); + }); + + // Dispatch menu::Confirm (enter) — should emit execute: false even in terminal mode. + prompt_editor.update(cx, |editor, cx| { + editor.handle_confirm(false, cx); + }); + + let events = events.borrow(); + assert_eq!(events.len(), 1, "Expected exactly one event"); + assert!( + matches!( + events[0], + PromptEditorEvent::ConfirmRequested { execute: false } + ), + "Expected ConfirmRequested with execute: false" + ); + } +}