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

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>
This commit is contained in:
Kunall Banerjee 2026-04-22 05:50:01 -04:00 committed by GitHub
parent 2ca94a6032
commit 6b27522843
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

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