mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
@@ -7,6 +7,13 @@ use crossterm::event::KeyEvent;
|
||||
|
||||
use super::CancellationEvent;
|
||||
|
||||
/// Reason an active bottom-pane view finished.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ViewCompletion {
|
||||
Accepted,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Trait implemented by every view that can be shown in the bottom pane.
|
||||
pub(crate) trait BottomPaneView: Renderable {
|
||||
/// Handle a key event while the view is active. A redraw is always
|
||||
@@ -18,6 +25,19 @@ pub(crate) trait BottomPaneView: Renderable {
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the completion reason once the view has finished.
|
||||
fn completion(&self) -> Option<ViewCompletion> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Return true when this view should be removed after a child view is accepted.
|
||||
fn dismiss_after_child_accept(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Clear any pending child-flow cleanup marker after a child view is cancelled.
|
||||
fn clear_dismiss_after_child_accept(&mut self) {}
|
||||
|
||||
/// Stable identifier for views that need external refreshes while open.
|
||||
fn view_id(&self) -> Option<&'static str> {
|
||||
None
|
||||
|
||||
@@ -18,6 +18,7 @@ use super::popup_consts::standard_popup_hint_line;
|
||||
|
||||
use super::CancellationEvent;
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::bottom_pane_view::ViewCompletion;
|
||||
use super::textarea::TextArea;
|
||||
use super::textarea::TextAreaState;
|
||||
|
||||
@@ -34,7 +35,7 @@ pub(crate) struct CustomPromptView {
|
||||
// UI state
|
||||
textarea: TextArea,
|
||||
textarea_state: RefCell<TextAreaState>,
|
||||
complete: bool,
|
||||
completion: Option<ViewCompletion>,
|
||||
}
|
||||
|
||||
impl CustomPromptView {
|
||||
@@ -58,7 +59,7 @@ impl CustomPromptView {
|
||||
on_submit,
|
||||
textarea,
|
||||
textarea_state: RefCell::new(TextAreaState::default()),
|
||||
complete: false,
|
||||
completion: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +80,7 @@ impl BottomPaneView for CustomPromptView {
|
||||
let text = self.textarea.text().trim().to_string();
|
||||
if !text.is_empty() {
|
||||
(self.on_submit)(text);
|
||||
self.complete = true;
|
||||
self.completion = Some(ViewCompletion::Accepted);
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
@@ -95,12 +96,16 @@ impl BottomPaneView for CustomPromptView {
|
||||
}
|
||||
|
||||
fn on_ctrl_c(&mut self) -> CancellationEvent {
|
||||
self.complete = true;
|
||||
self.completion = Some(ViewCompletion::Cancelled);
|
||||
CancellationEvent::Handled
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
self.complete
|
||||
self.completion.is_some()
|
||||
}
|
||||
|
||||
fn completion(&self) -> Option<ViewCompletion> {
|
||||
self.completion
|
||||
}
|
||||
|
||||
fn handle_paste(&mut self, pasted: String) -> bool {
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::render::renderable::Renderable;
|
||||
|
||||
use super::CancellationEvent;
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::bottom_pane_view::ViewCompletion;
|
||||
use super::popup_consts::MAX_POPUP_ROWS;
|
||||
use super::scroll_state::ScrollState;
|
||||
pub(crate) use super::selection_popup_common::ColumnWidthMode;
|
||||
@@ -121,6 +122,7 @@ pub(crate) struct SelectionItem {
|
||||
pub is_disabled: bool,
|
||||
pub actions: Vec<SelectionAction>,
|
||||
pub dismiss_on_select: bool,
|
||||
pub dismiss_parent_on_child_accept: bool,
|
||||
pub search_value: Option<String>,
|
||||
pub disabled_reason: Option<String>,
|
||||
}
|
||||
@@ -211,7 +213,8 @@ pub(crate) struct ListSelectionView {
|
||||
footer_hint: Option<Line<'static>>,
|
||||
items: Vec<SelectionItem>,
|
||||
state: ScrollState,
|
||||
complete: bool,
|
||||
completion: Option<ViewCompletion>,
|
||||
dismiss_after_child_accept: bool,
|
||||
app_event_tx: AppEventSender,
|
||||
is_searchable: bool,
|
||||
search_query: String,
|
||||
@@ -259,7 +262,8 @@ impl ListSelectionView {
|
||||
footer_hint: params.footer_hint,
|
||||
items: params.items,
|
||||
state: ScrollState::new(),
|
||||
complete: false,
|
||||
completion: None,
|
||||
dismiss_after_child_accept: false,
|
||||
app_event_tx,
|
||||
is_searchable: params.is_searchable,
|
||||
search_query: String::new(),
|
||||
@@ -458,13 +462,15 @@ impl ListSelectionView {
|
||||
act(&self.app_event_tx);
|
||||
}
|
||||
if item.dismiss_on_select {
|
||||
self.complete = true;
|
||||
self.completion = Some(ViewCompletion::Accepted);
|
||||
} else if item.dismiss_parent_on_child_accept {
|
||||
self.dismiss_after_child_accept = true;
|
||||
}
|
||||
} else if selected_item.is_none() {
|
||||
if let Some(cb) = &self.on_cancel {
|
||||
cb(&self.app_event_tx);
|
||||
}
|
||||
self.complete = true;
|
||||
self.completion = Some(ViewCompletion::Cancelled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,7 +678,19 @@ impl BottomPaneView for ListSelectionView {
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
self.complete
|
||||
self.completion.is_some()
|
||||
}
|
||||
|
||||
fn completion(&self) -> Option<ViewCompletion> {
|
||||
self.completion
|
||||
}
|
||||
|
||||
fn dismiss_after_child_accept(&self) -> bool {
|
||||
self.dismiss_after_child_accept
|
||||
}
|
||||
|
||||
fn clear_dismiss_after_child_accept(&mut self) {
|
||||
self.dismiss_after_child_accept = false;
|
||||
}
|
||||
|
||||
fn view_id(&self) -> Option<&'static str> {
|
||||
@@ -687,7 +705,7 @@ impl BottomPaneView for ListSelectionView {
|
||||
if let Some(cb) = &self.on_cancel {
|
||||
cb(&self.app_event_tx);
|
||||
}
|
||||
self.complete = true;
|
||||
self.completion = Some(ViewCompletion::Cancelled);
|
||||
CancellationEvent::Handled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::render::renderable::Renderable;
|
||||
use crate::render::renderable::RenderableItem;
|
||||
use crate::tui::FrameRequester;
|
||||
use bottom_pane_view::BottomPaneView;
|
||||
use bottom_pane_view::ViewCompletion;
|
||||
use codex_features::Features;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
@@ -380,8 +381,25 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn pop_active_view(&mut self) {
|
||||
fn pop_active_view_with_completion(&mut self, completion: Option<ViewCompletion>) {
|
||||
if self.view_stack.pop().is_some() {
|
||||
match completion {
|
||||
Some(ViewCompletion::Accepted) => {
|
||||
while self
|
||||
.view_stack
|
||||
.last()
|
||||
.is_some_and(|view| view.dismiss_after_child_accept())
|
||||
{
|
||||
self.view_stack.pop();
|
||||
}
|
||||
}
|
||||
Some(ViewCompletion::Cancelled) => {
|
||||
if let Some(view) = self.view_stack.last_mut() {
|
||||
view.clear_dismiss_after_child_accept();
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
self.on_view_stack_depth_decreased();
|
||||
}
|
||||
}
|
||||
@@ -403,7 +421,7 @@ impl BottomPane {
|
||||
// We need three pieces of information after routing the key:
|
||||
// whether Esc completed the view, whether the view finished for any
|
||||
// reason, and whether a paste-burst timer should be scheduled.
|
||||
let (ctrl_c_completed, view_complete, view_in_paste_burst) = {
|
||||
let (ctrl_c_completed, view_complete, completion, view_in_paste_burst) = {
|
||||
let last_index = self.view_stack.len() - 1;
|
||||
let view = &mut self.view_stack[last_index];
|
||||
let prefer_esc =
|
||||
@@ -413,22 +431,27 @@ impl BottomPane {
|
||||
&& matches!(view.on_ctrl_c(), CancellationEvent::Handled)
|
||||
&& view.is_complete();
|
||||
if ctrl_c_completed {
|
||||
(true, true, false)
|
||||
(true, true, view.completion(), false)
|
||||
} else {
|
||||
view.handle_key_event(key_event);
|
||||
(false, view.is_complete(), view.is_in_paste_burst())
|
||||
(
|
||||
false,
|
||||
view.is_complete(),
|
||||
view.completion(),
|
||||
view.is_in_paste_burst(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if ctrl_c_completed {
|
||||
self.pop_active_view();
|
||||
self.pop_active_view_with_completion(completion);
|
||||
if let Some(next_view) = self.view_stack.last()
|
||||
&& next_view.is_in_paste_burst()
|
||||
{
|
||||
self.request_redraw_in(ChatComposer::recommended_paste_flush_delay());
|
||||
}
|
||||
} else if view_complete {
|
||||
self.pop_active_view();
|
||||
self.pop_active_view_with_completion(completion);
|
||||
} else if view_in_paste_burst {
|
||||
self.request_redraw_in(ChatComposer::recommended_paste_flush_delay());
|
||||
}
|
||||
@@ -481,9 +504,10 @@ impl BottomPane {
|
||||
if let Some(view) = self.view_stack.last_mut() {
|
||||
let event = view.on_ctrl_c();
|
||||
let view_complete = view.is_complete();
|
||||
let completion = view.completion();
|
||||
if matches!(event, CancellationEvent::Handled) {
|
||||
if view_complete {
|
||||
self.pop_active_view();
|
||||
self.pop_active_view_with_completion(completion);
|
||||
}
|
||||
self.show_quit_shortcut_hint(key_hint::ctrl(KeyCode::Char('c')));
|
||||
self.request_redraw();
|
||||
|
||||
@@ -7953,6 +7953,7 @@ impl ChatWidget {
|
||||
is_default: preset.is_default,
|
||||
actions,
|
||||
dismiss_on_select: single_supported_effort,
|
||||
dismiss_parent_on_child_accept: !single_supported_effort,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -10476,6 +10477,7 @@ impl ChatWidget {
|
||||
}
|
||||
})],
|
||||
dismiss_on_select: false,
|
||||
dismiss_parent_on_child_accept: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
@@ -10501,6 +10503,7 @@ impl ChatWidget {
|
||||
}
|
||||
})],
|
||||
dismiss_on_select: false,
|
||||
dismiss_parent_on_child_accept: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
@@ -10510,6 +10513,7 @@ impl ChatWidget {
|
||||
tx.send(AppEvent::OpenReviewCustomPrompt);
|
||||
})],
|
||||
dismiss_on_select: false,
|
||||
dismiss_parent_on_child_accept: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user