mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
feedback diagnostics
This commit is contained in:
308
codex-rs/core/src/feedback_diagnostics.rs
Normal file
308
codex-rs/core/src/feedback_diagnostics.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user