feedback diagnostics

This commit is contained in:
Roy Han
2026-03-02 16:26:41 -08:00
parent 7709bf32a3
commit 7ede1d77f6
11 changed files with 556 additions and 27 deletions

View File

@@ -0,0 +1,308 @@
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use tempfile::Builder;
use tempfile::TempDir;
use url::Url;
const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
const OPENAI_BASE_URL_ENV_VAR: &str = "OPENAI_BASE_URL";
pub const FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME: &str = "codex-connectivity-diagnostics.txt";
const PROXY_ENV_VARS: &[&str] = &[
"HTTP_PROXY",
"http_proxy",
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
];
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FeedbackDiagnostics {
diagnostics: Vec<FeedbackDiagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FeedbackDiagnostic {
pub headline: String,
pub details: Vec<String>,
}
pub struct FeedbackDiagnosticsAttachment {
_dir: TempDir,
path: PathBuf,
}
impl FeedbackDiagnosticsAttachment {
pub fn path(&self) -> &Path {
&self.path
}
}
impl FeedbackDiagnostics {
pub fn collect_from_env() -> Self {
Self::collect_from_pairs(std::env::vars())
}
pub fn collect_from_pairs<I, K, V>(pairs: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let env = pairs
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect::<HashMap<_, _>>();
let mut diagnostics = Vec::new();
let proxy_details = PROXY_ENV_VARS
.iter()
.filter_map(|key| {
let value = env.get(*key)?.trim();
if value.is_empty() {
return None;
}
let detail = match sanitize_proxy_value(value) {
Some(sanitized) => format!("{key} = {sanitized}"),
None => format!("{key} = invalid value"),
};
Some(detail)
})
.collect::<Vec<_>>();
if !proxy_details.is_empty() {
diagnostics.push(FeedbackDiagnostic {
headline: "Proxy environment variables are set and may affect connectivity."
.to_string(),
details: proxy_details,
});
}
if let Some(value) = env.get(OPENAI_BASE_URL_ENV_VAR).map(String::as_str) {
let trimmed = value.trim();
if !trimmed.is_empty() && trimmed.trim_end_matches('/') != DEFAULT_OPENAI_BASE_URL {
let detail = match sanitize_url_for_display(trimmed) {
Some(sanitized) => format!("{OPENAI_BASE_URL_ENV_VAR} = {sanitized}"),
None => format!("{OPENAI_BASE_URL_ENV_VAR} = invalid value"),
};
diagnostics.push(FeedbackDiagnostic {
headline: "OPENAI_BASE_URL is set and may affect connectivity.".to_string(),
details: vec![detail],
});
}
}
Self { diagnostics }
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn diagnostics(&self) -> &[FeedbackDiagnostic] {
&self.diagnostics
}
pub fn attachment_text(&self) -> Option<String> {
if self.diagnostics.is_empty() {
return None;
}
let mut lines = vec!["Connectivity diagnostics".to_string(), String::new()];
for diagnostic in &self.diagnostics {
lines.push(format!("- {}", diagnostic.headline));
lines.extend(
diagnostic
.details
.iter()
.map(|detail| format!(" - {detail}")),
);
}
Some(lines.join("\n"))
}
pub fn write_temp_attachment(&self) -> io::Result<Option<FeedbackDiagnosticsAttachment>> {
let Some(text) = self.attachment_text() else {
return Ok(None);
};
let dir = Builder::new()
.prefix("codex-connectivity-diagnostics-")
.tempdir()?;
let path = dir.path().join(FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME);
fs::write(&path, text)?;
Ok(Some(FeedbackDiagnosticsAttachment { _dir: dir, path }))
}
}
pub fn sanitize_url_for_display(raw: &str) -> Option<String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
let Ok(mut url) = Url::parse(trimmed) else {
return None;
};
let _ = url.set_username("");
let _ = url.set_password(None);
url.set_query(None);
url.set_fragment(None);
Some(url.to_string().trim_end_matches('/').to_string()).filter(|value| !value.is_empty())
}
fn sanitize_proxy_value(raw: &str) -> Option<String> {
if raw.contains("://") {
return sanitize_url_for_display(raw);
}
sanitize_url_for_display(&format!("http://{raw}"))
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use std::ffi::OsStr;
use std::fs;
use super::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME;
use super::FeedbackDiagnostic;
use super::FeedbackDiagnostics;
use super::sanitize_url_for_display;
#[test]
fn collect_from_pairs_returns_empty_when_no_diagnostics_are_present() {
let diagnostics = FeedbackDiagnostics::collect_from_pairs(Vec::<(String, String)>::new());
assert_eq!(diagnostics, FeedbackDiagnostics::default());
assert_eq!(diagnostics.attachment_text(), None);
}
#[test]
fn collect_from_pairs_reports_proxy_env_vars_in_fixed_order() {
let diagnostics = FeedbackDiagnostics::collect_from_pairs([
("HTTPS_PROXY", "https://secure-proxy.example.com:443"),
("HTTP_PROXY", "proxy.example.com:8080"),
("ALL_PROXY", "socks5h://all-proxy.example.com:1080"),
]);
assert_eq!(
diagnostics,
FeedbackDiagnostics {
diagnostics: vec![FeedbackDiagnostic {
headline: "Proxy environment variables are set and may affect connectivity."
.to_string(),
details: vec![
"HTTP_PROXY = http://proxy.example.com:8080".to_string(),
"HTTPS_PROXY = https://secure-proxy.example.com".to_string(),
"ALL_PROXY = socks5h://all-proxy.example.com:1080".to_string(),
],
}],
}
);
}
#[test]
fn collect_from_pairs_reports_invalid_proxy_values_without_echoing_them() {
let diagnostics =
FeedbackDiagnostics::collect_from_pairs([("HTTP_PROXY", "not a valid\nproxy")]);
assert_eq!(
diagnostics,
FeedbackDiagnostics {
diagnostics: vec![FeedbackDiagnostic {
headline: "Proxy environment variables are set and may affect connectivity."
.to_string(),
details: vec!["HTTP_PROXY = invalid value".to_string()],
}],
}
);
}
#[test]
fn collect_from_pairs_reports_non_default_openai_base_url() {
let diagnostics = FeedbackDiagnostics::collect_from_pairs([(
"OPENAI_BASE_URL",
"https://example.com/v1",
)]);
assert_eq!(
diagnostics,
FeedbackDiagnostics {
diagnostics: vec![FeedbackDiagnostic {
headline: "OPENAI_BASE_URL is set and may affect connectivity.".to_string(),
details: vec!["OPENAI_BASE_URL = https://example.com/v1".to_string()],
}],
}
);
}
#[test]
fn collect_from_pairs_ignores_default_openai_base_url() {
let diagnostics = FeedbackDiagnostics::collect_from_pairs([(
"OPENAI_BASE_URL",
"https://api.openai.com/v1/",
)]);
assert_eq!(diagnostics, FeedbackDiagnostics::default());
}
#[test]
fn collect_from_pairs_reports_invalid_openai_base_url_without_echoing_it() {
let diagnostics =
FeedbackDiagnostics::collect_from_pairs([("OPENAI_BASE_URL", "not a valid\nurl")]);
assert_eq!(
diagnostics,
FeedbackDiagnostics {
diagnostics: vec![FeedbackDiagnostic {
headline: "OPENAI_BASE_URL is set and may affect connectivity.".to_string(),
details: vec!["OPENAI_BASE_URL = invalid value".to_string()],
}],
}
);
}
#[test]
fn sanitize_url_for_display_strips_credentials_query_and_fragment() {
let sanitized = sanitize_url_for_display(
"https://user:password@example.com:8443/v1?token=secret#fragment",
);
assert_eq!(sanitized, Some("https://example.com:8443/v1".to_string()));
}
#[test]
fn write_temp_attachment_persists_sanitized_text() {
let diagnostics = FeedbackDiagnostics::collect_from_pairs([
(
"HTTP_PROXY",
"https://user:password@proxy.example.com:8443?secret=1",
),
("OPENAI_BASE_URL", "https://example.com/v1?token=secret"),
]);
let attachment = diagnostics
.write_temp_attachment()
.expect("attachment should be written")
.expect("attachment should be present");
let contents =
fs::read_to_string(attachment.path()).expect("attachment should be readable");
assert_eq!(
attachment.path().file_name(),
Some(OsStr::new(FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME))
);
assert_eq!(
contents,
"Connectivity diagnostics\n\n- Proxy environment variables are set and may affect connectivity.\n - HTTP_PROXY = https://proxy.example.com:8443\n- OPENAI_BASE_URL is set and may affect connectivity.\n - OPENAI_BASE_URL = https://example.com/v1"
.to_string()
);
}
}

View File

@@ -37,6 +37,7 @@ pub mod exec_env;
mod exec_policy;
pub mod external_agent_config;
pub mod features;
pub mod feedback_diagnostics;
mod file_watcher;
mod flags;
pub mod git_info;

View File

@@ -1,6 +1,9 @@
use std::cell::RefCell;
use std::path::PathBuf;
use codex_core::feedback_diagnostics::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME;
use codex_core::feedback_diagnostics::FeedbackDiagnostics;
use codex_core::feedback_diagnostics::FeedbackDiagnosticsAttachment;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
@@ -19,6 +22,8 @@ use crate::app_event::FeedbackCategory;
use crate::app_event_sender::AppEventSender;
use crate::history_cell;
use crate::render::renderable::Renderable;
use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_lines;
use codex_protocol::protocol::SessionSource;
use super::CancellationEvent;
@@ -48,6 +53,7 @@ pub(crate) struct FeedbackNoteView {
category: FeedbackCategory,
snapshot: codex_feedback::CodexLogSnapshot,
rollout_path: Option<PathBuf>,
diagnostics: FeedbackDiagnostics,
app_event_tx: AppEventSender,
include_logs: bool,
feedback_audience: FeedbackAudience,
@@ -58,11 +64,17 @@ pub(crate) struct FeedbackNoteView {
complete: bool,
}
struct PreparedFeedbackAttachments {
attachment_paths: Vec<PathBuf>,
_diagnostics_attachment: Option<FeedbackDiagnosticsAttachment>,
}
impl FeedbackNoteView {
pub(crate) fn new(
category: FeedbackCategory,
snapshot: codex_feedback::CodexLogSnapshot,
rollout_path: Option<PathBuf>,
diagnostics: FeedbackDiagnostics,
app_event_tx: AppEventSender,
include_logs: bool,
feedback_audience: FeedbackAudience,
@@ -71,6 +83,7 @@ impl FeedbackNoteView {
category,
snapshot,
rollout_path,
diagnostics,
app_event_tx,
include_logs,
feedback_audience,
@@ -87,11 +100,7 @@ impl FeedbackNoteView {
} else {
Some(note.as_str())
};
let log_file_paths = if self.include_logs {
self.rollout_path.iter().cloned().collect::<Vec<_>>()
} else {
Vec::new()
};
let attachments = self.prepare_feedback_attachments();
let classification = feedback_classification(self.category);
let mut thread_id = self.snapshot.thread_id.clone();
@@ -100,7 +109,7 @@ impl FeedbackNoteView {
classification,
reason_opt,
self.include_logs,
&log_file_paths,
&attachments.attachment_paths,
Some(SessionSource::Cli),
);
@@ -216,21 +225,21 @@ impl BottomPaneView for FeedbackNoteView {
impl Renderable for FeedbackNoteView {
fn desired_height(&self, width: u16) -> u16 {
1u16 + self.input_height(width) + 3u16
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 top_line_count = 1u16; // title only
let textarea_rect = Rect {
x: area.x.saturating_add(2),
y: area.y.saturating_add(top_line_count).saturating_add(1),
y: area.y.saturating_add(intro_height).saturating_add(1),
width: area.width.saturating_sub(2),
height: text_area_height,
};
@@ -243,23 +252,26 @@ impl Renderable for FeedbackNoteView {
return;
}
let (title, placeholder) = feedback_title_and_placeholder(self.category);
let intro_lines = self.intro_lines(area.width);
let (_, placeholder) = feedback_title_and_placeholder(self.category);
let input_height = self.input_height(area.width);
// Title line
let title_area = Rect {
x: area.x,
y: area.y,
width: area.width,
height: 1,
};
let title_spans: Vec<Span<'static>> = vec![gutter(), title.bold()];
Paragraph::new(Line::from(title_spans)).render(title_area, buf);
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(1),
y: area.y.saturating_add(intro_lines.len() as u16),
width: area.width,
height: input_height,
};
@@ -333,6 +345,77 @@ impl FeedbackNoteView {
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<Line<'static>> {
let (title, _) = feedback_title_and_placeholder(self.category);
let mut lines = vec![
Line::from(vec![gutter(), title.bold()]),
Line::from(vec![gutter()]),
Line::from(vec![gutter(), "Connectivity diagnostics".bold()]),
];
lines.extend(self.diagnostics_lines(width));
lines
}
fn diagnostics_lines(&self, width: u16) -> Vec<Line<'static>> {
let width = usize::from(width.max(1));
if self.diagnostics.is_empty() {
let options = RtOptions::new(width)
.initial_indent(Line::from(vec![gutter(), " ".dim()]))
.subsequent_indent(Line::from(vec![gutter(), " ".dim()]));
return word_wrap_lines(["No connectivity diagnostics detected.".dim()], options);
}
let headline_options = RtOptions::new(width)
.initial_indent(Line::from(vec![gutter(), " - ".into()]))
.subsequent_indent(Line::from(vec![gutter(), " ".into()]));
let detail_options = RtOptions::new(width)
.initial_indent(Line::from(vec![gutter(), " - ".dim()]))
.subsequent_indent(Line::from(vec![gutter(), " ".into()]));
let mut lines = Vec::new();
for diagnostic in self.diagnostics.diagnostics() {
lines.extend(word_wrap_lines(
[Line::from(diagnostic.headline.clone())],
headline_options.clone(),
));
for detail in &diagnostic.details {
lines.extend(word_wrap_lines(
[Line::from(detail.clone())],
detail_options.clone(),
));
}
}
lines
}
fn prepare_feedback_attachments(&self) -> PreparedFeedbackAttachments {
let mut attachment_paths = if self.include_logs {
self.rollout_path.iter().cloned().collect::<Vec<_>>()
} else {
Vec::new()
};
let diagnostics_attachment = if self.include_logs {
match self.diagnostics.write_temp_attachment() {
Ok(attachment) => attachment,
Err(err) => {
tracing::warn!(error = %err, "failed to write diagnostics attachment");
None
}
}
} else {
None
};
if let Some(attachment) = diagnostics_attachment.as_ref() {
attachment_paths.push(attachment.path().to_path_buf());
}
PreparedFeedbackAttachments {
attachment_paths,
_diagnostics_attachment: diagnostics_attachment,
}
}
}
fn gutter() -> Span<'static> {
@@ -512,7 +595,7 @@ pub(crate) fn feedback_upload_consent_params(
let mut header_lines: Vec<Box<dyn crate::render::renderable::Renderable>> = vec![
Line::from("Upload logs?".bold()).into(),
Line::from("").into(),
Line::from("The following files will be sent:".dim()).into(),
Line::from("If you choose Yes, the following files may be sent:".dim()).into(),
Line::from(vec!["".into(), "codex-logs.log".into()]).into(),
];
if let Some(path) = rollout_path.as_deref()
@@ -520,6 +603,14 @@ pub(crate) fn feedback_upload_consent_params(
{
header_lines.push(Line::from(vec!["".into(), name.into()]).into());
}
header_lines.push(
Line::from(vec![
"".into(),
FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME.into(),
" (if connectivity diagnostics are detected)".dim(),
])
.into(),
);
super::SelectionViewParams {
footer_hint: Some(standard_popup_hint_line()),
@@ -527,7 +618,7 @@ pub(crate) fn feedback_upload_consent_params(
super::SelectionItem {
name: "Yes".to_string(),
description: Some(
"Share the current Codex session logs with the team for troubleshooting."
"Share the current Codex session logs and any generated diagnostics attachment."
.to_string(),
),
actions: vec![yes_action],
@@ -536,7 +627,7 @@ pub(crate) fn feedback_upload_consent_params(
},
super::SelectionItem {
name: "No".to_string(),
description: Some("".to_string()),
description: Some("Send feedback without any attached files.".to_string()),
actions: vec![no_action],
dismiss_on_select: true,
..Default::default()
@@ -554,6 +645,9 @@ mod tests {
use super::*;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use pretty_assertions::assert_eq;
use std::ffi::OsStr;
use std::fs;
fn render(view: &FeedbackNoteView, width: u16) -> String {
let height = view.desired_height(width);
@@ -593,6 +687,7 @@ mod tests {
category,
snapshot,
None,
FeedbackDiagnostics::default(),
tx,
true,
FeedbackAudience::External,
@@ -634,6 +729,90 @@ mod tests {
insta::assert_snapshot!("feedback_view_safety_check", rendered);
}
#[test]
fn feedback_view_with_connectivity_diagnostics() {
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let snapshot = codex_feedback::CodexFeedback::new().snapshot(None);
let diagnostics = FeedbackDiagnostics::collect_from_pairs([
("HTTP_PROXY", "proxy.example.com:8080"),
("OPENAI_BASE_URL", "https://example.com/v1"),
]);
let view = FeedbackNoteView::new(
FeedbackCategory::Bug,
snapshot,
None,
diagnostics,
tx,
false,
FeedbackAudience::External,
);
let rendered = render(&view, 60);
insta::assert_snapshot!("feedback_view_with_connectivity_diagnostics", rendered);
}
#[test]
fn prepare_feedback_attachments_skips_diagnostics_without_logs() {
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let snapshot = codex_feedback::CodexFeedback::new().snapshot(None);
let diagnostics = FeedbackDiagnostics::collect_from_pairs([(
"OPENAI_BASE_URL",
"https://example.com/v1",
)]);
let view = FeedbackNoteView::new(
FeedbackCategory::Other,
snapshot,
None,
diagnostics,
tx,
false,
FeedbackAudience::External,
);
let attachments = view.prepare_feedback_attachments();
assert_eq!(attachments.attachment_paths, Vec::<PathBuf>::new());
assert!(attachments._diagnostics_attachment.is_none());
}
#[test]
fn prepare_feedback_attachments_includes_diagnostics_with_logs() {
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let snapshot = codex_feedback::CodexFeedback::new().snapshot(None);
let diagnostics = FeedbackDiagnostics::collect_from_pairs([(
"OPENAI_BASE_URL",
"https://example.com/v1",
)]);
let view = FeedbackNoteView::new(
FeedbackCategory::Other,
snapshot,
None,
diagnostics,
tx,
true,
FeedbackAudience::External,
);
let attachments = view.prepare_feedback_attachments();
assert_eq!(attachments.attachment_paths.len(), 1);
assert_eq!(
attachments.attachment_paths[0].file_name(),
Some(OsStr::new(FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME))
);
assert!(
attachments._diagnostics_attachment.is_some(),
"diagnostics attachment should stay alive until upload completes"
);
let contents = fs::read_to_string(&attachments.attachment_paths[0])
.expect("diagnostics attachment should be readable");
assert_eq!(
contents,
"Connectivity diagnostics\n\n- OPENAI_BASE_URL is set and may affect connectivity.\n - OPENAI_BASE_URL = https://example.com/v1"
);
}
#[test]
fn issue_url_available_for_bug_bad_result_safety_check_and_other() {
let bug_url = issue_url_for_category(

View File

@@ -1,9 +1,13 @@
---
source: tui/src/bottom_pane/feedback_view.rs
assertion_line: 698
expression: rendered
---
▌ Tell us more (bad result)
▌ Connectivity diagnostics
▌ No connectivity diagnostics detected.
▌ (optional) Write a short description to help us further
Press enter to confirm or esc to go back

View File

@@ -1,9 +1,13 @@
---
source: tui/src/bottom_pane/feedback_view.rs
assertion_line: 712
expression: rendered
---
▌ Tell us more (bug)
▌ Connectivity diagnostics
▌ No connectivity diagnostics detected.
▌ (optional) Write a short description to help us further
Press enter to confirm or esc to go back

View File

@@ -1,9 +1,13 @@
---
source: tui/src/bottom_pane/feedback_view.rs
assertion_line: 705
expression: rendered
---
▌ Tell us more (good result)
▌ Connectivity diagnostics
▌ No connectivity diagnostics detected.
▌ (optional) Write a short description to help us further
Press enter to confirm or esc to go back

View File

@@ -1,9 +1,13 @@
---
source: tui/src/bottom_pane/feedback_view.rs
assertion_line: 719
expression: rendered
---
▌ Tell us more (other)
▌ Connectivity diagnostics
▌ No connectivity diagnostics detected.
▌ (optional) Write a short description to help us further
Press enter to confirm or esc to go back

View File

@@ -1,9 +1,13 @@
---
source: tui/src/bottom_pane/feedback_view.rs
assertion_line: 726
expression: rendered
---
▌ Tell us more (safety check)
▌ Connectivity diagnostics
▌ No connectivity diagnostics detected.
▌ (optional) Share what was refused and why it should have b
Press enter to confirm or esc to go back

View File

@@ -0,0 +1,17 @@
---
source: tui/src/bottom_pane/feedback_view.rs
assertion_line: 749
expression: rendered
---
▌ Tell us more (bug)
▌ Connectivity diagnostics
▌ - Proxy environment variables are set and may affect
▌ connectivity.
▌ - HTTP_PROXY = http://proxy.example.com:8080
▌ - OPENAI_BASE_URL is set and may affect connectivity.
▌ - OPENAI_BASE_URL = https://example.com/v1
▌ (optional) Write a short description to help us further
Press enter to confirm or esc to go back

View File

@@ -1251,6 +1251,7 @@ impl ChatWidget {
) {
// Build a fresh snapshot at the time of opening the note overlay.
let snapshot = self.feedback.snapshot(self.thread_id);
let diagnostics = codex_core::feedback_diagnostics::FeedbackDiagnostics::collect_from_env();
let rollout = if include_logs {
self.current_rollout_path.clone()
} else {
@@ -1260,6 +1261,7 @@ impl ChatWidget {
category,
snapshot,
rollout,
diagnostics,
self.app_event_tx.clone(),
include_logs,
self.feedback_audience,

View File

@@ -1,14 +1,16 @@
---
source: tui/src/chatwidget/tests.rs
assertion_line: 6401
expression: popup
---
Upload logs?
The following files will be sent:
If you choose Yes, the following files may be sent:
• codex-logs.log
• codex-connectivity-diagnostics.txt (if connectivity diagnostics are dete
1. Yes Share the current Codex session logs with the team for
troubleshooting.
2. No
1. Yes Share the current Codex session logs and any generated diagnostics
attachment.
2. No Send feedback without any attached files.
Press enter to confirm or esc to go back