diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index c4effd6a52..2996a24d35 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -815,6 +815,9 @@ impl App { AppEvent::EvalCaptureIntroContinue => { self.chat_widget.on_eval_capture_intro_continue(); } + AppEvent::EvalCaptureSendConsent { upload } => { + self.chat_widget.on_eval_capture_send_consent(upload); + } AppEvent::OpenEvalCaptureNotes { start_marker } => { self.chat_widget.open_eval_capture_notes(start_marker); } @@ -829,12 +832,6 @@ impl App { what_good_looks_like, ); } - AppEvent::EvalCaptureUpload { case_id, path } => { - self.chat_widget.upload_eval_capture_bundle(case_id, path); - } - AppEvent::EvalCaptureUploadSkipped { case_id, path } => { - self.chat_widget.eval_capture_upload_skipped(case_id, path); - } AppEvent::LaunchExternalEditor => { if self.chat_widget.external_editor_state() == ExternalEditorState::Active { self.launch_external_editor(tui).await; diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 608c5ac174..da83d02d6d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -191,6 +191,12 @@ pub(crate) enum AppEvent { /// Dismiss the eval capture intro screen and continue to the start picker. EvalCaptureIntroContinue, + /// Record whether the user consented to upload the eval sample bundle once captured. + /// This happens before the start message picker so the full flow is consented up front. + EvalCaptureSendConsent { + upload: bool, + }, + /// Open the freeform notes entry for eval capture. OpenEvalCaptureNotes { start_marker: EvalCaptureStartMarker, @@ -203,18 +209,6 @@ pub(crate) enum AppEvent { what_good_looks_like: String, }, - /// Upload the previously captured eval case bundle to the team. - EvalCaptureUpload { - case_id: String, - path: String, - }, - - /// Skip uploading the previously captured eval case bundle. - EvalCaptureUploadSkipped { - case_id: String, - path: String, - }, - /// Launch the external editor after a normal draw has completed. LaunchExternalEditor, } diff --git a/codex-rs/tui/src/bottom_pane/eval_capture_view.rs b/codex-rs/tui/src/bottom_pane/eval_capture_view.rs index 4148509722..eac50ab57c 100644 --- a/codex-rs/tui/src/bottom_pane/eval_capture_view.rs +++ b/codex-rs/tui/src/bottom_pane/eval_capture_view.rs @@ -83,37 +83,25 @@ pub(crate) fn eval_capture_intro_params( } } -pub(crate) fn eval_capture_upload_consent_params( +pub(crate) fn eval_capture_send_consent_params( app_event_tx: AppEventSender, - case_id: String, - path: String, ) -> super::SelectionViewParams { let upload_action: super::SelectionAction = Box::new({ let tx = app_event_tx.clone(); - let case_id = case_id.clone(); - let path = path.clone(); move |_sender: &AppEventSender| { - tx.send(AppEvent::EvalCaptureUpload { - case_id: case_id.clone(), - path: path.clone(), - }); + tx.send(AppEvent::EvalCaptureSendConsent { upload: true }); } }); let skip_action: super::SelectionAction = Box::new({ let tx = app_event_tx; - let case_id = case_id.clone(); - let path = path.clone(); move |_sender: &AppEventSender| { - tx.send(AppEvent::EvalCaptureUploadSkipped { - case_id: case_id.clone(), - path: path.clone(), - }); + tx.send(AppEvent::EvalCaptureSendConsent { upload: false }); } }); let header_lines: Vec> = vec![ - Line::from("Upload eval sample?".bold()).into(), + Line::from("Send eval sample to the codex team?".bold()).into(), Line::from("").into(), Line::from("If you choose Yes, it will upload the full bundle".dim()).into(), Line::from("to the team:".dim()).into(), @@ -360,7 +348,7 @@ impl Renderable for EvalCaptureNotesView { width: area.width, height: 1, }; - Paragraph::new(Line::from("Enter to continue • Esc to cancel".dim())) + Paragraph::new(Line::from("Enter to continue • Esc to go back".dim())) .render(hint_area, buf); } } @@ -371,7 +359,9 @@ impl EvalCaptureNotesView { let subtitle = "Please be specific, and can refer to any aspect of model behavior, including tool calls, code changes, or messages."; let usable_width = width.saturating_sub(2).max(1) as usize; let mut lines = Vec::new(); - lines.push(Line::from(title.bold())); + for line in textwrap::wrap(title, usable_width) { + lines.push(Line::from(line.into_owned().bold())); + } for line in textwrap::wrap(subtitle, usable_width) { lines.push(Line::from(line.into_owned().dim())); } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index b59ac01ac1..945f43367a 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -37,8 +37,8 @@ pub(crate) use list_selection_view::SelectionViewParams; mod feedback_view; pub(crate) use eval_capture_view::EvalCaptureNotesView; pub(crate) use eval_capture_view::eval_capture_intro_params; +pub(crate) use eval_capture_view::eval_capture_send_consent_params; pub(crate) use eval_capture_view::eval_capture_start_picker_params; -pub(crate) use eval_capture_view::eval_capture_upload_consent_params; pub(crate) use feedback_view::feedback_bad_result_fork_params; pub(crate) use feedback_view::feedback_selection_params; pub(crate) use feedback_view::feedback_upload_consent_params; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 62092d1a80..6a63e75dbc 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -370,6 +370,9 @@ pub(crate) struct ChatWidget { feedback: codex_feedback::CodexFeedback, // Current session rollout path (if known) current_rollout_path: Option, + // Whether the user consented to upload the captured eval sample to the team. + // This is captured before the start picker so the flow is consented up front. + eval_capture_send_to_team: Option, external_editor_state: ExternalEditorState, } @@ -518,8 +521,9 @@ impl ChatWidget { } pub(crate) fn open_eval_capture_intro(&mut self) { + self.eval_capture_send_to_team = None; if !crate::eval_capture_state::should_show_eval_capture_intro(&self.config.codex_home) { - self.open_eval_capture_start_picker(); + self.open_eval_capture_send_consent(); return; } @@ -535,6 +539,18 @@ impl ChatWidget { self.add_error_message(format!("Failed to persist eval capture preference: {err}")); } + self.open_eval_capture_send_consent(); + } + + fn open_eval_capture_send_consent(&mut self) { + let params = + crate::bottom_pane::eval_capture_send_consent_params(self.app_event_tx.clone()); + self.bottom_pane.show_selection_view(params); + self.request_redraw(); + } + + pub(crate) fn on_eval_capture_send_consent(&mut self, upload: bool) { + self.eval_capture_send_to_team = Some(upload); self.open_eval_capture_start_picker(); } @@ -838,13 +854,11 @@ impl ChatWidget { crate::history_cell::PlainHistoryCell::new(lines), ))); - let params = crate::bottom_pane::eval_capture_upload_consent_params( - self.app_event_tx.clone(), - case_id, - path, - ); - self.bottom_pane.show_selection_view(params); - self.request_redraw(); + if self.eval_capture_send_to_team.unwrap_or(false) { + self.upload_eval_capture_bundle(case_id, path); + } else { + self.eval_capture_upload_skipped(case_id, path); + } } Err(err) => { self.app_event_tx @@ -1949,6 +1963,7 @@ impl ChatWidget { last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, + eval_capture_send_to_team: None, external_editor_state: ExternalEditorState::Closed, }; @@ -2036,6 +2051,7 @@ impl ChatWidget { last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, + eval_capture_send_to_team: None, external_editor_state: ExternalEditorState::Closed, }; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__eval_capture_upload_consent_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__eval_capture_upload_consent_popup.snap index b2eac67449..c78f96fea7 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__eval_capture_upload_consent_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__eval_capture_upload_consent_popup.snap @@ -1,9 +1,8 @@ --- source: tui/src/chatwidget/tests.rs -assertion_line: 2206 expression: popup --- - Upload eval sample? + Send eval sample to the codex team? If you choose Yes, it will upload the full bundle to the team: diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index bfe46edc69..e08ad69d93 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -407,6 +407,7 @@ async fn make_chatwidget_manual( last_rendered_width: std::cell::Cell::new(None), feedback: codex_feedback::CodexFeedback::new(), current_rollout_path: None, + eval_capture_send_to_team: None, external_editor_state: ExternalEditorState::Closed, }; (widget, rx, op_rx) @@ -2194,11 +2195,7 @@ async fn eval_capture_intro_popup_snapshot() { async fn eval_capture_upload_consent_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - let params = crate::bottom_pane::eval_capture_upload_consent_params( - chat.app_event_tx.clone(), - "case-2026-01-01T00-00-00-repo-000001".to_string(), - "/tmp/eval-case/case-2026-01-01T00-00-00-repo-000001".to_string(), - ); + let params = crate::bottom_pane::eval_capture_send_consent_params(chat.app_event_tx.clone()); chat.bottom_pane.show_selection_view(params); chat.request_redraw();