@@ -166,6 +166,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.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<T: 'static> PromptEditor<T> {
}
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
+ self.handle_confirm(false, cx);
+ }
+
+ fn secondary_confirm(
+ &mut self,
+ _: &menu::SecondaryConfirm,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let execute = matches!(self.mode, PromptEditorMode::Terminal { .. });
+ self.handle_confirm(execute, cx);
+ }
+
+ fn handle_confirm(&mut self, execute: bool, cx: &mut Context<Self>) {
match self.codegen_status(cx) {
CodegenStatus::Idle => {
self.fire_started_telemetry(cx);
@@ -541,7 +556,7 @@ impl<T: 'static> PromptEditor<T> {
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<Workspace>,
+ cx: &mut VisualTestContext,
+ ) -> Entity<PromptEditor<TerminalCodegen>> {
+ 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<RefCell<Vec<PromptEditorEvent>>> = 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<RefCell<Vec<PromptEditorEvent>>> = 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"
+ );
+ }
+}