Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions codex-rs/tui/src/bottom_pane/request_user_input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,8 @@ impl RequestUserInputOverlay {
if self.selected_option_index().is_some() && !notes_visible {
tips.push(FooterTip::highlighted("tab to add notes"));
}
if self.selected_option_index().is_some() && notes_visible && self.focus_is_notes() {
tips.push(FooterTip::new("tab to clear notes"));
if self.selected_option_index().is_some() && notes_visible {
tips.push(FooterTip::new("tab or esc to clear notes"));
}
}

Expand All @@ -449,7 +449,9 @@ impl RequestUserInputOverlay {
tips.push(FooterTip::new("ctrl + n next question"));
}
}
tips.push(FooterTip::new("esc to interrupt"));
if !(self.has_options() && notes_visible) {
tips.push(FooterTip::new("esc to interrupt"));
}
tips
}

Expand Down Expand Up @@ -663,6 +665,23 @@ impl RequestUserInputOverlay {
self.sync_composer_placeholder();
}

fn clear_notes_and_focus_options(&mut self) {
if !self.has_options() {
return;
}
if let Some(answer) = self.current_answer_mut() {
answer.draft = ComposerDraft::default();
answer.answer_committed = false;
answer.notes_visible = false;
}
self.pending_submission_draft = None;
self.composer
.set_text_content(String::new(), Vec::new(), Vec::new());
self.composer.move_cursor_to_end();
self.focus = Focus::Options;
self.sync_composer_placeholder();
}

/// Ensure there is a selection before allowing notes entry.
fn ensure_selected_for_notes(&mut self) {
if let Some(answer) = self.current_answer_mut() {
Expand Down Expand Up @@ -976,6 +995,10 @@ impl BottomPaneView for RequestUserInputOverlay {
}

if matches!(key_event.code, KeyCode::Esc) {
if self.has_options() && self.notes_ui_visible() {
self.clear_notes_and_focus_options();
return;
}
// TODO: Emit interrupted request_user_input results (including committed answers)
// once core supports persisting them reliably without follow-up turn issues.
self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt));
Expand Down Expand Up @@ -1093,16 +1116,7 @@ impl BottomPaneView for RequestUserInputOverlay {
Focus::Notes => {
let notes_empty = self.composer.current_text_with_pending().trim().is_empty();
if self.has_options() && matches!(key_event.code, KeyCode::Tab) {
if let Some(answer) = self.current_answer_mut() {
answer.draft = ComposerDraft::default();
answer.answer_committed = false;
answer.notes_visible = false;
}
self.composer
.set_text_content(String::new(), Vec::new(), Vec::new());
self.composer.move_cursor_to_end();
self.focus = Focus::Options;
self.sync_composer_placeholder();
self.clear_notes_and_focus_options();
return;
}
if self.has_options() && matches!(key_event.code, KeyCode::Backspace) && notes_empty
Expand Down Expand Up @@ -1753,7 +1767,7 @@ mod tests {
}

#[test]
fn esc_in_notes_mode_interrupts() {
fn esc_in_notes_mode_clears_notes_and_hides_ui() {
let (tx, mut rx) = test_sender();
let mut overlay = RequestUserInputOverlay::new(
request_event("turn-1", vec![question_with_options("q1", "Pick one")]),
Expand All @@ -1769,12 +1783,19 @@ mod tests {
overlay.handle_key_event(KeyEvent::from(KeyCode::Tab));
overlay.handle_key_event(KeyEvent::from(KeyCode::Esc));

assert_eq!(overlay.done, true);
expect_interrupt_only(&mut rx);
let answer = overlay.current_answer().expect("answer missing");
assert_eq!(overlay.done, false);
assert!(matches!(overlay.focus, Focus::Options));
assert_eq!(overlay.notes_ui_visible(), false);
assert_eq!(overlay.composer.current_text_with_pending(), "");
assert_eq!(answer.draft.text, "");
assert_eq!(answer.options_state.selected_idx, Some(0));
assert_eq!(answer.answer_committed, false);
assert!(rx.try_recv().is_err());
}

#[test]
fn esc_in_notes_mode_interrupts_with_notes_visible() {
fn esc_in_notes_mode_with_text_clears_notes_and_hides_ui() {
let (tx, mut rx) = test_sender();
let mut overlay = RequestUserInputOverlay::new(
request_event("turn-1", vec![question_with_options("q1", "Pick one")]),
Expand All @@ -1791,8 +1812,15 @@ mod tests {
overlay.handle_key_event(KeyEvent::from(KeyCode::Char('a')));
overlay.handle_key_event(KeyEvent::from(KeyCode::Esc));

assert_eq!(overlay.done, true);
expect_interrupt_only(&mut rx);
let answer = overlay.current_answer().expect("answer missing");
assert_eq!(overlay.done, false);
assert!(matches!(overlay.focus, Focus::Options));
assert_eq!(overlay.notes_ui_visible(), false);
assert_eq!(overlay.composer.current_text_with_pending(), "");
assert_eq!(answer.draft.text, "");
assert_eq!(answer.options_state.selected_idx, Some(0));
assert_eq!(answer.answer_committed, false);
assert!(rx.try_recv().is_err());
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: tui/src/bottom_pane/request_user_input/mod.rs
assertion_line: 2321
expression: "render_snapshot(&overlay, area)"
---

Expand All @@ -16,4 +17,4 @@ expression: "render_snapshot(&overlay, area)"



tab to clear notes | enter to submit answer | esc to interrupt
tab or esc to clear notes | enter to submit answer
4 changes: 3 additions & 1 deletion docs/tui-request-user-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ friction for freeform input.
- Enter advances to the next question.
- Enter on the last question submits all answers.
- PageUp/PageDown navigate across questions (when multiple are present).
- Esc interrupts the run.
- Esc interrupts the run in option selection mode.
- When notes are open for an option question, Tab or Esc clears notes and returns
to option selection.

## Layout priorities

Expand Down
Loading