mirror of
https://github.com/openai/codex.git
synced 2026-04-30 09:26:44 +00:00
Refine request_user_input TUI interactions and option UX (#10025)
## Summary Overhaul the ask‑user‑questions TUI to support “Other/None” answers, better notes handling, improved option selection UX, and a safer submission flow with confirmation for unanswered questions. Multiple choice (number keys for quick selection, up/down or jk for cycling through options): <img width="856" height="169" alt="Screenshot 2026-01-27 at 7 22 29 PM" src="https://github.com/user-attachments/assets/cabd1b0e-25e0-4859-bd8f-9941192ca274" /> Tab to add notes: <img width="856" height="197" alt="Screenshot 2026-01-27 at 7 22 45 PM" src="https://github.com/user-attachments/assets/a807db5e-e966-412c-af91-6edc60062f35" /> Freeform (also note enter tooltip is highlighted on last question to indicate questions UI will be exited upon submission): <img width="854" height="112" alt="Screenshot 2026-01-27 at 7 23 13 PM" src="https://github.com/user-attachments/assets/2e7b88bf-062b-4b9f-a9da-c9d8c8a59643" /> Confirmation dialogue (submitting with unanswered questions): <img width="854" height="126" alt="Screenshot 2026-01-27 at 7 23 29 PM" src="https://github.com/user-attachments/assets/93965c8f-54ac-45bc-a660-9625bcd101f8" /> ## Key Changes - **Options UI refresh** - Render options as numbered entries; allow number keys to select & submit. - Remove “Option X/Y” header and allow the question UI height to expand naturally. - Keep spacing between question, options, and notes even when notes are visible. - Hide the title line and render the question prompt in cyan **only when uncommitted**. - **“Other / None of the above” support** - Wire `isOther` to add “None of the above”. - Add guidance text: “Optionally, add details in notes (tab).” - **Notes composer UX** - Remove “Notes” heading; place composer directly under the selected option. - Preserve pending paste placeholders across question navigation and after submission. - Ctrl+C clears notes **only when the notes composer has focus**. - Ctrl+C now triggers an immediate redraw so the clear is visible. - **Committed vs uncommitted state** - Introduce a unified `answer_committed` flag per question. - Editing notes (including adding text or pastes) marks the answer uncommitted. - Changing the option highlight (j/k, up/down) marks the answer uncommitted. - Clearing options (Backspace/Delete) also clears pending notes. - Question prompt turns cyan only when the answer is uncommitted. - **Submission safety & confirmation** - Only submit notes/freeform text once explicitly committed. - Last-question submit with unanswered questions shows a confirmation dialog. - Confirmation options: 1. **Proceed** (default) 2. **Go back** - Description reflects count: “Submit with N unanswered question(s).” - Esc/Backspace in confirmation returns to first unanswered question. - Ctrl+C in confirmation interrupts and exits the overlay. - **Footer hints** - Cyan highlight restored for “enter to submit answer” / “enter to submit all”. ## Codex author `codex fork 019c00ed-323a-7000-bdb5-9f9c5a635bd9`
This commit is contained in:
committed by
GitHub
parent
74bd6d7178
commit
96386755b6
@@ -269,7 +269,10 @@ impl BottomPane {
|
||||
let (ctrl_c_completed, view_complete, view_in_paste_burst) = {
|
||||
let last_index = self.view_stack.len() - 1;
|
||||
let view = &mut self.view_stack[last_index];
|
||||
let prefer_esc =
|
||||
key_event.code == KeyCode::Esc && view.prefer_esc_to_handle_key_event();
|
||||
let ctrl_c_completed = key_event.code == KeyCode::Esc
|
||||
&& !prefer_esc
|
||||
&& matches!(view.on_ctrl_c(), CancellationEvent::Handled)
|
||||
&& view.is_complete();
|
||||
if ctrl_c_completed {
|
||||
@@ -338,6 +341,7 @@ impl BottomPane {
|
||||
self.on_active_view_complete();
|
||||
}
|
||||
self.show_quit_shortcut_hint(key_hint::ctrl(KeyCode::Char('c')));
|
||||
self.request_redraw();
|
||||
}
|
||||
event
|
||||
} else if self.composer_is_empty() {
|
||||
@@ -346,6 +350,7 @@ impl BottomPane {
|
||||
self.view_stack.pop();
|
||||
self.clear_composer_for_ctrl_c();
|
||||
self.show_quit_shortcut_hint(key_hint::ctrl(KeyCode::Char('c')));
|
||||
self.request_redraw();
|
||||
CancellationEvent::Handled
|
||||
}
|
||||
}
|
||||
@@ -815,7 +820,9 @@ mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use std::cell::Cell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
fn snapshot_buffer(buf: &Buffer) -> String {
|
||||
@@ -1242,4 +1249,63 @@ mod tests {
|
||||
"expected Esc to send Op::Interrupt while a task is running"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_routes_to_handle_key_event_when_requested() {
|
||||
#[derive(Default)]
|
||||
struct EscRoutingView {
|
||||
on_ctrl_c_calls: Rc<Cell<usize>>,
|
||||
handle_calls: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Renderable for EscRoutingView {
|
||||
fn render(&self, _area: Rect, _buf: &mut Buffer) {}
|
||||
|
||||
fn desired_height(&self, _width: u16) -> u16 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl BottomPaneView for EscRoutingView {
|
||||
fn handle_key_event(&mut self, _key_event: KeyEvent) {
|
||||
self.handle_calls
|
||||
.set(self.handle_calls.get().saturating_add(1));
|
||||
}
|
||||
|
||||
fn on_ctrl_c(&mut self) -> CancellationEvent {
|
||||
self.on_ctrl_c_calls
|
||||
.set(self.on_ctrl_c_calls.get().saturating_add(1));
|
||||
CancellationEvent::Handled
|
||||
}
|
||||
|
||||
fn prefer_esc_to_handle_key_event(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
let on_ctrl_c_calls = Rc::new(Cell::new(0));
|
||||
let handle_calls = Rc::new(Cell::new(0));
|
||||
pane.push_view(Box::new(EscRoutingView {
|
||||
on_ctrl_c_calls: Rc::clone(&on_ctrl_c_calls),
|
||||
handle_calls: Rc::clone(&handle_calls),
|
||||
}));
|
||||
|
||||
pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(on_ctrl_c_calls.get(), 0);
|
||||
assert_eq!(handle_calls.get(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user