From 47b0f7d540e9abf932e9b518ab306e389744998e Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 4 Sep 2026 15:49:40 +0000 Subject: [PATCH] Extract the note input view into its own module (#42792) ## What changed Move the optional note input and its focused tests out of the broader submission module. Update the bottom-pane export and snapshot metadata to match the new module layout without changing submission behavior. GitOrigin-RevId: 383ad052b47571f8a46e1aa6182261c370008be0 --- .../tui/src/bottom_pane/feedback_note_view.rs | 278 +++++++++++++ .../bottom_pane/feedback_note_view_tests.rs | 145 +++++++ codex-rs/tui/src/bottom_pane/feedback_view.rs | 385 +----------------- codex-rs/tui/src/bottom_pane/mod.rs | 3 +- ...iew__tests__feedback_view_bad_result.snap} | 2 +- ..._note_view__tests__feedback_view_bug.snap} | 2 +- ...ew__tests__feedback_view_good_result.snap} | 2 +- ...ote_view__tests__feedback_view_other.snap} | 2 +- ...w__tests__feedback_view_safety_check.snap} | 2 +- ...k_view_with_connectivity_diagnostics.snap} | 2 +- 10 files changed, 434 insertions(+), 389 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/feedback_note_view.rs create mode 100644 codex-rs/tui/src/bottom_pane/feedback_note_view_tests.rs rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__feedback_view__tests__feedback_view_bad_result.snap => codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bad_result.snap} (74%) rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__feedback_view__tests__feedback_view_with_connectivity_diagnostics.snap => codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bug.snap} (73%) rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__feedback_view__tests__feedback_view_good_result.snap => codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_good_result.snap} (74%) rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__feedback_view__tests__feedback_view_other.snap => codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_other.snap} (74%) rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__feedback_view__tests__feedback_view_safety_check.snap => codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_safety_check.snap} (75%) rename codex-rs/tui/src/bottom_pane/snapshots/{codex_tui__bottom_pane__feedback_view__tests__feedback_view_bug.snap => codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_with_connectivity_diagnostics.snap} (73%) diff --git a/codex-rs/tui/src/bottom_pane/feedback_note_view.rs b/codex-rs/tui/src/bottom_pane/feedback_note_view.rs new file mode 100644 index 0000000000..ad9c29dbd0 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/feedback_note_view.rs @@ -0,0 +1,278 @@ +//! Collects an optional feedback note and submits it with the selected log consent. + +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyModifiers; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::Clear; +use ratatui::widgets::Paragraph; +use ratatui::widgets::StatefulWidgetRef; +use ratatui::widgets::Widget; +use std::cell::RefCell; + +use crate::app_event::AppEvent; +use crate::app_event::FeedbackCategory; +use crate::app_event_sender::AppEventSender; +use crate::render::renderable::Renderable; + +use super::CancellationEvent; +use super::bottom_pane_view::BottomPaneView; +use super::popup_consts::standard_popup_hint_line; +use super::textarea::TextArea; +use super::textarea::TextAreaState; + +/// Minimal input overlay to collect an optional feedback note, then submit it +/// through the app-server-managed feedback flow. +pub(crate) struct FeedbackNoteView { + category: FeedbackCategory, + turn_id: Option, + app_event_tx: AppEventSender, + include_logs: bool, + + // UI state + textarea: TextArea, + textarea_state: RefCell, + complete: bool, +} + +impl FeedbackNoteView { + pub(crate) fn new( + category: FeedbackCategory, + turn_id: Option, + app_event_tx: AppEventSender, + include_logs: bool, + ) -> Self { + Self { + category, + turn_id, + app_event_tx, + include_logs, + textarea: TextArea::new(), + textarea_state: RefCell::new(TextAreaState::default()), + complete: false, + } + } + + fn submit(&mut self) { + let note = self.textarea.text().trim().to_string(); + let reason = if note.is_empty() { None } else { Some(note) }; + self.app_event_tx.send(AppEvent::SubmitFeedback { + category: self.category, + reason, + turn_id: self.turn_id.clone(), + include_logs: self.include_logs, + }); + self.complete = true; + } +} + +impl BottomPaneView for FeedbackNoteView { + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event { + KeyEvent { + code: KeyCode::Esc, .. + } => { + self.on_ctrl_c(); + } + KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + .. + } => { + self.submit(); + } + KeyEvent { + code: KeyCode::Enter, + .. + } => { + self.textarea.input(key_event); + } + other => { + self.textarea.input(other); + } + } + } + + fn on_ctrl_c(&mut self) -> CancellationEvent { + self.complete = true; + CancellationEvent::Handled + } + + fn is_complete(&self) -> bool { + self.complete + } + + fn handle_paste(&mut self, pasted: String) -> bool { + if pasted.is_empty() { + return false; + } + self.textarea.insert_str(&pasted); + true + } +} + +impl Renderable for FeedbackNoteView { + fn desired_height(&self, width: u16) -> u16 { + self.intro_lines(width).len() as u16 + self.input_height(width) + 2u16 + } + + fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { + if area.height < 2 || area.width <= 2 { + return None; + } + let intro_height = self.intro_lines(area.width).len() as u16; + let text_area_height = self.input_height(area.width).saturating_sub(1); + if text_area_height == 0 { + return None; + } + let textarea_rect = Rect { + x: area.x.saturating_add(2), + y: area.y.saturating_add(intro_height).saturating_add(1), + width: area.width.saturating_sub(2), + height: text_area_height, + }; + let state = *self.textarea_state.borrow(); + self.textarea.cursor_pos_with_state(textarea_rect, state) + } + + fn render(&self, area: Rect, buf: &mut Buffer) { + if area.height == 0 || area.width == 0 { + return; + } + + let intro_lines = self.intro_lines(area.width); + let (_, placeholder) = feedback_title_and_placeholder(self.category); + let input_height = self.input_height(area.width); + + for (offset, line) in intro_lines.iter().enumerate() { + Paragraph::new(line.clone()).render( + Rect { + x: area.x, + y: area.y.saturating_add(offset as u16), + width: area.width, + height: 1, + }, + buf, + ); + } + + // Input line + let input_area = Rect { + x: area.x, + y: area.y.saturating_add(intro_lines.len() as u16), + width: area.width, + height: input_height, + }; + if input_area.width >= 2 { + for row in 0..input_area.height { + Paragraph::new(Line::from(vec![gutter()])).render( + Rect { + x: input_area.x, + y: input_area.y.saturating_add(row), + width: 2, + height: 1, + }, + buf, + ); + } + + let text_area_height = input_area.height.saturating_sub(1); + if text_area_height > 0 { + if input_area.width > 2 { + let blank_rect = Rect { + x: input_area.x.saturating_add(2), + y: input_area.y, + width: input_area.width.saturating_sub(2), + height: 1, + }; + Clear.render(blank_rect, buf); + } + let textarea_rect = Rect { + x: input_area.x.saturating_add(2), + y: input_area.y.saturating_add(1), + width: input_area.width.saturating_sub(2), + height: text_area_height, + }; + let mut state = self.textarea_state.borrow_mut(); + StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); + if self.textarea.text().is_empty() { + Paragraph::new(Line::from(placeholder.dim())).render(textarea_rect, buf); + } + } + } + + let hint_blank_y = input_area.y.saturating_add(input_height); + if hint_blank_y < area.y.saturating_add(area.height) { + let blank_area = Rect { + x: area.x, + y: hint_blank_y, + width: area.width, + height: 1, + }; + Clear.render(blank_area, buf); + } + + let hint_y = hint_blank_y.saturating_add(1); + if hint_y < area.y.saturating_add(area.height) { + Paragraph::new(standard_popup_hint_line()).render( + Rect { + x: area.x, + y: hint_y, + width: area.width, + height: 1, + }, + buf, + ); + } + } +} + +impl FeedbackNoteView { + fn input_height(&self, width: u16) -> u16 { + let usable_width = width.saturating_sub(2); + let text_height = self.textarea.desired_height(usable_width).clamp(1, 8); + text_height.saturating_add(1).min(9) + } + + fn intro_lines(&self, _width: u16) -> Vec> { + let (title, _) = feedback_title_and_placeholder(self.category); + vec![Line::from(vec![gutter(), title.bold()])] + } +} + +fn gutter() -> Span<'static> { + "▌ ".cyan() +} + +fn feedback_title_and_placeholder(category: FeedbackCategory) -> (String, String) { + match category { + FeedbackCategory::BadResult => ( + "Tell us more (bad result)".to_string(), + "(optional) Write a short description to help us further".to_string(), + ), + FeedbackCategory::GoodResult => ( + "Tell us more (good result)".to_string(), + "(optional) Write a short description to help us further".to_string(), + ), + FeedbackCategory::Bug => ( + "Tell us more (bug)".to_string(), + "(optional) Write a short description to help us further".to_string(), + ), + FeedbackCategory::SafetyCheck => ( + "Tell us more (safety check)".to_string(), + "(optional) Share what was refused and why it should have been allowed".to_string(), + ), + FeedbackCategory::Other => ( + "Tell us more (other)".to_string(), + "(optional) Write a short description to help us further".to_string(), + ), + } +} + +#[cfg(test)] +#[path = "feedback_note_view_tests.rs"] +mod tests; diff --git a/codex-rs/tui/src/bottom_pane/feedback_note_view_tests.rs b/codex-rs/tui/src/bottom_pane/feedback_note_view_tests.rs new file mode 100644 index 0000000000..e6926ce91a --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/feedback_note_view_tests.rs @@ -0,0 +1,145 @@ +use super::*; +use pretty_assertions::assert_eq; + +fn render(view: &FeedbackNoteView, width: u16) -> String { + let height = view.desired_height(width); + let area = Rect::new(0, 0, width, height); + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); + render_buffer(area, &buf) +} + +fn render_buffer(area: Rect, buf: &Buffer) -> String { + let mut lines: Vec = (0..area.height) + .map(|row| { + let mut line = String::new(); + for col in 0..area.width { + let symbol = buf[(area.x + col, area.y + row)].symbol(); + if symbol.is_empty() { + line.push(' '); + } else { + line.push_str(symbol); + } + } + line.trim_end().to_string() + }) + .collect(); + + while lines.first().is_some_and(|l| l.trim().is_empty()) { + lines.remove(0); + } + while lines.last().is_some_and(|l| l.trim().is_empty()) { + lines.pop(); + } + lines.join("\n") +} + +fn make_view(category: FeedbackCategory) -> FeedbackNoteView { + let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + FeedbackNoteView::new( + category, /*turn_id*/ None, tx, /*include_logs*/ true, + ) +} + +#[test] +fn feedback_view_bad_result() { + let view = make_view(FeedbackCategory::BadResult); + let rendered = render(&view, /*width*/ 60); + insta::assert_snapshot!("feedback_view_bad_result", rendered); +} + +#[test] +fn feedback_view_good_result() { + let view = make_view(FeedbackCategory::GoodResult); + let rendered = render(&view, /*width*/ 60); + insta::assert_snapshot!("feedback_view_good_result", rendered); +} + +#[test] +fn feedback_view_bug() { + let view = make_view(FeedbackCategory::Bug); + let rendered = render(&view, /*width*/ 60); + insta::assert_snapshot!("feedback_view_bug", rendered); +} + +#[test] +fn feedback_view_other() { + let view = make_view(FeedbackCategory::Other); + let rendered = render(&view, /*width*/ 60); + insta::assert_snapshot!("feedback_view_other", rendered); +} + +#[test] +fn feedback_view_safety_check() { + let view = make_view(FeedbackCategory::SafetyCheck); + let rendered = render(&view, /*width*/ 60); + insta::assert_snapshot!("feedback_view_safety_check", rendered); +} + +#[test] +fn feedback_view_with_connectivity_diagnostics() { + let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let view = FeedbackNoteView::new( + FeedbackCategory::Bug, + /*turn_id*/ None, + tx, + /*include_logs*/ false, + ); + let rendered = render(&view, /*width*/ 60); + + insta::assert_snapshot!("feedback_view_with_connectivity_diagnostics", rendered); +} + +#[test] +fn submit_feedback_emits_submit_event_with_trimmed_note() { + let (tx_raw, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut view = FeedbackNoteView::new( + FeedbackCategory::Bug, + Some("turn-123".to_string()), + tx, + /*include_logs*/ true, + ); + view.textarea.insert_str(" something broke "); + + view.submit(); + + let event = rx.try_recv().expect("submit feedback event"); + assert!(matches!( + event, + AppEvent::SubmitFeedback { + category: FeedbackCategory::Bug, + reason: Some(reason), + turn_id: Some(turn_id), + include_logs: true, + } if reason == "something broke" && turn_id == "turn-123" + )); + assert_eq!(view.is_complete(), true); +} + +#[test] +fn submit_feedback_omits_empty_note() { + let (tx_raw, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut view = FeedbackNoteView::new( + FeedbackCategory::GoodResult, + /*turn_id*/ None, + tx, + /*include_logs*/ false, + ); + + view.submit(); + + let event = rx.try_recv().expect("submit feedback event"); + assert!(matches!( + event, + AppEvent::SubmitFeedback { + category: FeedbackCategory::GoodResult, + reason: None, + turn_id: None, + include_logs: false, + } + )); +} diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index 15874531bc..d17c689431 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -4,31 +4,15 @@ use codex_feedback::DOCTOR_REPORT_ATTACHMENT_FILENAME; use codex_feedback::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME; use codex_feedback::FeedbackDiagnostics; use codex_feedback::WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME; -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use crossterm::event::KeyModifiers; -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; -use ratatui::widgets::Clear; -use ratatui::widgets::Paragraph; -use ratatui::widgets::StatefulWidgetRef; -use ratatui::widgets::Widget; -use std::cell::RefCell; use crate::app_event::AppEvent; use crate::app_event::FeedbackCategory; use crate::app_event_sender::AppEventSender; use crate::history_cell; -use crate::render::renderable::Renderable; -use super::CancellationEvent; -use super::bottom_pane_view::BottomPaneView; use super::popup_consts::standard_popup_hint_line; -use super::textarea::TextArea; -use super::textarea::TextAreaState; const BASE_CLI_BUG_ISSUE_URL: &str = "https://github.com/openai/codex/issues/new?template=3-cli.yml"; @@ -45,225 +29,6 @@ pub(crate) enum FeedbackAudience { External, } -/// Minimal input overlay to collect an optional feedback note, then submit it -/// through the app-server-managed feedback flow. -pub(crate) struct FeedbackNoteView { - category: FeedbackCategory, - turn_id: Option, - app_event_tx: AppEventSender, - include_logs: bool, - - // UI state - textarea: TextArea, - textarea_state: RefCell, - complete: bool, -} - -impl FeedbackNoteView { - pub(crate) fn new( - category: FeedbackCategory, - turn_id: Option, - app_event_tx: AppEventSender, - include_logs: bool, - ) -> Self { - Self { - category, - turn_id, - app_event_tx, - include_logs, - textarea: TextArea::new(), - textarea_state: RefCell::new(TextAreaState::default()), - complete: false, - } - } - - fn submit(&mut self) { - let note = self.textarea.text().trim().to_string(); - let reason = if note.is_empty() { None } else { Some(note) }; - self.app_event_tx.send(AppEvent::SubmitFeedback { - category: self.category, - reason, - turn_id: self.turn_id.clone(), - include_logs: self.include_logs, - }); - self.complete = true; - } -} - -impl BottomPaneView for FeedbackNoteView { - fn handle_key_event(&mut self, key_event: KeyEvent) { - match key_event { - KeyEvent { - code: KeyCode::Esc, .. - } => { - self.on_ctrl_c(); - } - KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::NONE, - .. - } => { - self.submit(); - } - KeyEvent { - code: KeyCode::Enter, - .. - } => { - self.textarea.input(key_event); - } - other => { - self.textarea.input(other); - } - } - } - - fn on_ctrl_c(&mut self) -> CancellationEvent { - self.complete = true; - CancellationEvent::Handled - } - - fn is_complete(&self) -> bool { - self.complete - } - - fn handle_paste(&mut self, pasted: String) -> bool { - if pasted.is_empty() { - return false; - } - self.textarea.insert_str(&pasted); - true - } -} - -impl Renderable for FeedbackNoteView { - fn desired_height(&self, width: u16) -> u16 { - self.intro_lines(width).len() as u16 + self.input_height(width) + 2u16 - } - - fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { - if area.height < 2 || area.width <= 2 { - return None; - } - let intro_height = self.intro_lines(area.width).len() as u16; - let text_area_height = self.input_height(area.width).saturating_sub(1); - if text_area_height == 0 { - return None; - } - let textarea_rect = Rect { - x: area.x.saturating_add(2), - y: area.y.saturating_add(intro_height).saturating_add(1), - width: area.width.saturating_sub(2), - height: text_area_height, - }; - let state = *self.textarea_state.borrow(); - self.textarea.cursor_pos_with_state(textarea_rect, state) - } - - fn render(&self, area: Rect, buf: &mut Buffer) { - if area.height == 0 || area.width == 0 { - return; - } - - let intro_lines = self.intro_lines(area.width); - let (_, placeholder) = feedback_title_and_placeholder(self.category); - let input_height = self.input_height(area.width); - - for (offset, line) in intro_lines.iter().enumerate() { - Paragraph::new(line.clone()).render( - Rect { - x: area.x, - y: area.y.saturating_add(offset as u16), - width: area.width, - height: 1, - }, - buf, - ); - } - - // Input line - let input_area = Rect { - x: area.x, - y: area.y.saturating_add(intro_lines.len() as u16), - width: area.width, - height: input_height, - }; - if input_area.width >= 2 { - for row in 0..input_area.height { - Paragraph::new(Line::from(vec![gutter()])).render( - Rect { - x: input_area.x, - y: input_area.y.saturating_add(row), - width: 2, - height: 1, - }, - buf, - ); - } - - let text_area_height = input_area.height.saturating_sub(1); - if text_area_height > 0 { - if input_area.width > 2 { - let blank_rect = Rect { - x: input_area.x.saturating_add(2), - y: input_area.y, - width: input_area.width.saturating_sub(2), - height: 1, - }; - Clear.render(blank_rect, buf); - } - let textarea_rect = Rect { - x: input_area.x.saturating_add(2), - y: input_area.y.saturating_add(1), - width: input_area.width.saturating_sub(2), - height: text_area_height, - }; - let mut state = self.textarea_state.borrow_mut(); - StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); - if self.textarea.text().is_empty() { - Paragraph::new(Line::from(placeholder.dim())).render(textarea_rect, buf); - } - } - } - - let hint_blank_y = input_area.y.saturating_add(input_height); - if hint_blank_y < area.y.saturating_add(area.height) { - let blank_area = Rect { - x: area.x, - y: hint_blank_y, - width: area.width, - height: 1, - }; - Clear.render(blank_area, buf); - } - - let hint_y = hint_blank_y.saturating_add(1); - if hint_y < area.y.saturating_add(area.height) { - Paragraph::new(standard_popup_hint_line()).render( - Rect { - x: area.x, - y: hint_y, - width: area.width, - height: 1, - }, - buf, - ); - } - } -} - -impl FeedbackNoteView { - fn input_height(&self, width: u16) -> u16 { - let usable_width = width.saturating_sub(2); - let text_height = self.textarea.desired_height(usable_width).clamp(1, 8); - text_height.saturating_add(1).min(9) - } - - fn intro_lines(&self, _width: u16) -> Vec> { - let (title, _) = feedback_title_and_placeholder(self.category); - vec![Line::from(vec![gutter(), title.bold()])] - } -} - pub(crate) fn should_show_feedback_connectivity_details( category: FeedbackCategory, diagnostics: &FeedbackDiagnostics, @@ -271,35 +36,6 @@ pub(crate) fn should_show_feedback_connectivity_details( category != FeedbackCategory::GoodResult && !diagnostics.is_empty() } -fn gutter() -> Span<'static> { - "▌ ".cyan() -} - -fn feedback_title_and_placeholder(category: FeedbackCategory) -> (String, String) { - match category { - FeedbackCategory::BadResult => ( - "Tell us more (bad result)".to_string(), - "(optional) Write a short description to help us further".to_string(), - ), - FeedbackCategory::GoodResult => ( - "Tell us more (good result)".to_string(), - "(optional) Write a short description to help us further".to_string(), - ), - FeedbackCategory::Bug => ( - "Tell us more (bug)".to_string(), - "(optional) Write a short description to help us further".to_string(), - ), - FeedbackCategory::SafetyCheck => ( - "Tell us more (safety check)".to_string(), - "(optional) Share what was refused and why it should have been allowed".to_string(), - ), - FeedbackCategory::Other => ( - "Tell us more (other)".to_string(), - "(optional) Write a short description to help us further".to_string(), - ), - } -} - pub(crate) fn feedback_classification(category: FeedbackCategory) -> &'static str { match category { FeedbackCategory::BadResult => "bad_result", @@ -593,16 +329,11 @@ mod tests { use super::*; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; + use crate::render::renderable::Renderable; use codex_feedback::FeedbackDiagnostic; use pretty_assertions::assert_eq; - - fn render(view: &FeedbackNoteView, width: u16) -> String { - let height = view.desired_height(width); - let area = Rect::new(0, 0, width, height); - let mut buf = Buffer::empty(area); - view.render(area, &mut buf); - render_buffer(area, &buf) - } + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; fn render_renderable(renderable: &dyn Renderable, width: u16) -> String { let height = renderable.desired_height(width); @@ -652,64 +383,6 @@ mod tests { .join("\n") } - fn make_view(category: FeedbackCategory) -> FeedbackNoteView { - let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); - let tx = AppEventSender::new(tx_raw); - FeedbackNoteView::new( - category, /*turn_id*/ None, tx, /*include_logs*/ true, - ) - } - - #[test] - fn feedback_view_bad_result() { - let view = make_view(FeedbackCategory::BadResult); - let rendered = render(&view, /*width*/ 60); - insta::assert_snapshot!("feedback_view_bad_result", rendered); - } - - #[test] - fn feedback_view_good_result() { - let view = make_view(FeedbackCategory::GoodResult); - let rendered = render(&view, /*width*/ 60); - insta::assert_snapshot!("feedback_view_good_result", rendered); - } - - #[test] - fn feedback_view_bug() { - let view = make_view(FeedbackCategory::Bug); - let rendered = render(&view, /*width*/ 60); - insta::assert_snapshot!("feedback_view_bug", rendered); - } - - #[test] - fn feedback_view_other() { - let view = make_view(FeedbackCategory::Other); - let rendered = render(&view, /*width*/ 60); - insta::assert_snapshot!("feedback_view_other", rendered); - } - - #[test] - fn feedback_view_safety_check() { - let view = make_view(FeedbackCategory::SafetyCheck); - let rendered = render(&view, /*width*/ 60); - insta::assert_snapshot!("feedback_view_safety_check", rendered); - } - - #[test] - fn feedback_view_with_connectivity_diagnostics() { - let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); - let tx = AppEventSender::new(tx_raw); - let view = FeedbackNoteView::new( - FeedbackCategory::Bug, - /*turn_id*/ None, - tx, - /*include_logs*/ false, - ); - let rendered = render(&view, /*width*/ 60); - - insta::assert_snapshot!("feedback_view_with_connectivity_diagnostics", rendered); - } - #[test] fn feedback_upload_consent_lists_doctor_report() { let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); @@ -749,58 +422,6 @@ mod tests { ); } - #[test] - fn submit_feedback_emits_submit_event_with_trimmed_note() { - let (tx_raw, mut rx) = tokio::sync::mpsc::unbounded_channel::(); - let tx = AppEventSender::new(tx_raw); - let mut view = FeedbackNoteView::new( - FeedbackCategory::Bug, - Some("turn-123".to_string()), - tx, - /*include_logs*/ true, - ); - view.textarea.insert_str(" something broke "); - - view.submit(); - - let event = rx.try_recv().expect("submit feedback event"); - assert!(matches!( - event, - AppEvent::SubmitFeedback { - category: FeedbackCategory::Bug, - reason: Some(reason), - turn_id: Some(turn_id), - include_logs: true, - } if reason == "something broke" && turn_id == "turn-123" - )); - assert_eq!(view.is_complete(), true); - } - - #[test] - fn submit_feedback_omits_empty_note() { - let (tx_raw, mut rx) = tokio::sync::mpsc::unbounded_channel::(); - let tx = AppEventSender::new(tx_raw); - let mut view = FeedbackNoteView::new( - FeedbackCategory::GoodResult, - /*turn_id*/ None, - tx, - /*include_logs*/ false, - ); - - view.submit(); - - let event = rx.try_recv().expect("submit feedback event"); - assert!(matches!( - event, - AppEvent::SubmitFeedback { - category: FeedbackCategory::GoodResult, - reason: None, - turn_id: None, - include_logs: false, - } - )); - } - #[test] fn should_show_feedback_connectivity_details_only_for_non_good_result_with_diagnostics() { let diagnostics = FeedbackDiagnostics::new(vec![FeedbackDiagnostic { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 21d13cf1eb..1a297a1dbc 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -137,6 +137,7 @@ pub(crate) use list_selection_view::popup_content_width; pub(crate) use list_selection_view::side_by_side_layout_widths; pub(crate) use memories_settings_view::MemoriesSettingsView; use slash_commands::ServiceTierCommand; +mod feedback_note_view; mod feedback_view; mod hooks_browser_view; pub(crate) use feedback_view::FeedbackAudience; @@ -166,7 +167,7 @@ mod selection_tabs; mod startup; mod textarea; mod unified_exec_footer; -pub(crate) use feedback_view::FeedbackNoteView; +pub(crate) use feedback_note_view::FeedbackNoteView; pub(crate) use hooks_browser_view::HooksBrowserView; pub(crate) use selection_tabs::SelectionTab; diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_bad_result.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bad_result.snap similarity index 74% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_bad_result.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bad_result.snap index 465f0f9c4f..cbf67c36bc 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_bad_result.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bad_result.snap @@ -1,5 +1,5 @@ --- -source: tui/src/bottom_pane/feedback_view.rs +source: tui/src/bottom_pane/feedback_note_view_tests.rs expression: rendered --- ▌ Tell us more (bad result) diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_with_connectivity_diagnostics.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bug.snap similarity index 73% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_with_connectivity_diagnostics.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bug.snap index a0b5660135..468c1cb621 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_with_connectivity_diagnostics.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_bug.snap @@ -1,5 +1,5 @@ --- -source: tui/src/bottom_pane/feedback_view.rs +source: tui/src/bottom_pane/feedback_note_view_tests.rs expression: rendered --- ▌ Tell us more (bug) diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_good_result.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_good_result.snap similarity index 74% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_good_result.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_good_result.snap index 73074d61fa..cc8559fef9 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_good_result.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_good_result.snap @@ -1,5 +1,5 @@ --- -source: tui/src/bottom_pane/feedback_view.rs +source: tui/src/bottom_pane/feedback_note_view_tests.rs expression: rendered --- ▌ Tell us more (good result) diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_other.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_other.snap similarity index 74% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_other.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_other.snap index 80e4ffeffe..10100ee92a 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_other.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_other.snap @@ -1,5 +1,5 @@ --- -source: tui/src/bottom_pane/feedback_view.rs +source: tui/src/bottom_pane/feedback_note_view_tests.rs expression: rendered --- ▌ Tell us more (other) diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_safety_check.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_safety_check.snap similarity index 75% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_safety_check.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_safety_check.snap index 52148d0e86..c108f0f27c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_safety_check.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_safety_check.snap @@ -1,5 +1,5 @@ --- -source: tui/src/bottom_pane/feedback_view.rs +source: tui/src/bottom_pane/feedback_note_view_tests.rs expression: rendered --- ▌ Tell us more (safety check) diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_bug.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_with_connectivity_diagnostics.snap similarity index 73% rename from codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_bug.snap rename to codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_with_connectivity_diagnostics.snap index a0b5660135..468c1cb621 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_view_bug.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_note_view__tests__feedback_view_with_connectivity_diagnostics.snap @@ -1,5 +1,5 @@ --- -source: tui/src/bottom_pane/feedback_view.rs +source: tui/src/bottom_pane/feedback_note_view_tests.rs expression: rendered --- ▌ Tell us more (bug)