terminal: Handle `menu::SecondaryConfirm` properly (#48764)

Kunall Banerjee and Bennet Bo Fenner created

Closes #48642.

- [ ] Tests or screenshots needed? - Tests written by AI. Not sure if
the test suite is setup correctly. I’ll come back later and see if the
tests are utter BS or not.
- [ ] Code Reviewed
- [x] Manual QA


Release Notes:

- Handle `menu::SecondaryConfirm` properly in the terminal

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>

Change summary

crates/agent_ui/src/inline_prompt_editor.rs | 219 ++++++++++++++++++++++
1 file changed, 218 insertions(+), 1 deletion(-)

Detailed changes

crates/agent_ui/src/inline_prompt_editor.rs 🔗

@@ -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"
+        );
+    }
+}