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
21 changes: 21 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::app_event::WindowsSandboxEnableMode;
use crate::app_event::WindowsSandboxFallbackReason;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::FeedbackAudience;
use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionViewParams;
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
Expand Down Expand Up @@ -541,6 +542,7 @@ pub(crate) struct App {
/// transcript cells.
pub(crate) backtrack_render_pending: bool,
pub(crate) feedback: codex_feedback::CodexFeedback,
feedback_audience: FeedbackAudience,
/// Set when the user confirms an update; propagated on exit.
pub(crate) pending_update_action: Option<UpdateAction>,

Expand Down Expand Up @@ -599,6 +601,7 @@ impl App {
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
feedback_audience: self.feedback_audience,
model: Some(self.chat_widget.current_model().to_string()),
otel_manager: self.otel_manager.clone(),
}
Expand Down Expand Up @@ -957,6 +960,17 @@ impl App {

let auth = auth_manager.auth().await;
let auth_ref = auth.as_ref();
// Determine who should see internal Slack routing. We treat
// `@openai.com` emails as employees and default to `External` when the
// email is unavailable (for example, API key auth).
let feedback_audience = if auth_ref
.and_then(CodexAuth::get_account_email)
.is_some_and(|email| email.ends_with("@openai.com"))
{
FeedbackAudience::OpenAiEmployee
} else {
FeedbackAudience::External
};
let otel_manager = OtelManager::new(
ThreadId::new(),
model.as_str(),
Expand Down Expand Up @@ -987,6 +1001,7 @@ impl App {
models_manager: thread_manager.get_models_manager(),
feedback: feedback.clone(),
is_first_run,
feedback_audience,
model: Some(model.clone()),
otel_manager: otel_manager.clone(),
};
Expand Down Expand Up @@ -1015,6 +1030,7 @@ impl App {
models_manager: thread_manager.get_models_manager(),
feedback: feedback.clone(),
is_first_run,
feedback_audience,
model: config.model.clone(),
otel_manager: otel_manager.clone(),
};
Expand Down Expand Up @@ -1043,6 +1059,7 @@ impl App {
models_manager: thread_manager.get_models_manager(),
feedback: feedback.clone(),
is_first_run,
feedback_audience,
model: config.model.clone(),
otel_manager: otel_manager.clone(),
};
Expand Down Expand Up @@ -1078,6 +1095,7 @@ impl App {
backtrack: BacktrackState::default(),
backtrack_render_pending: false,
feedback: feedback.clone(),
feedback_audience,
pending_update_action: None,
suppress_shutdown_complete: false,
windows_sandbox: WindowsSandboxState::default(),
Expand Down Expand Up @@ -1268,6 +1286,7 @@ impl App {
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
feedback_audience: self.feedback_audience,
model: Some(model),
otel_manager: self.otel_manager.clone(),
};
Expand Down Expand Up @@ -2601,6 +2620,7 @@ mod tests {
backtrack: BacktrackState::default(),
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
feedback_audience: FeedbackAudience::External,
pending_update_action: None,
suppress_shutdown_complete: false,
windows_sandbox: WindowsSandboxState::default(),
Expand Down Expand Up @@ -2653,6 +2673,7 @@ mod tests {
backtrack: BacktrackState::default(),
backtrack_render_pending: false,
feedback: codex_feedback::CodexFeedback::new(),
feedback_audience: FeedbackAudience::External,
pending_update_action: None,
suppress_shutdown_complete: false,
windows_sandbox: WindowsSandboxState::default(),
Expand Down
150 changes: 117 additions & 33 deletions codex-rs/tui/src/bottom_pane/feedback_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ use super::textarea::TextAreaState;

const BASE_BUG_ISSUE_URL: &str =
"https://github.com/openai/codex/issues/new?template=2-bug-report.yml";
/// Internal routing link for employee feedback follow-ups. This must not be shown to external users.
const CODEX_FEEDBACK_INTERNAL_URL: &str = "http://go/codex-feedback-internal";

/// The target audience for feedback follow-up instructions.
///
/// This is used strictly for messaging/links after feedback upload completes. It
/// must not change feedback upload behavior itself.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FeedbackAudience {
OpenAiEmployee,
External,
}

/// Minimal input overlay to collect an optional feedback note, then upload
/// both logs and rollout with classification + metadata.
Expand All @@ -38,6 +50,7 @@ pub(crate) struct FeedbackNoteView {
rollout_path: Option<PathBuf>,
app_event_tx: AppEventSender,
include_logs: bool,
feedback_audience: FeedbackAudience,

// UI state
textarea: TextArea,
Expand All @@ -52,13 +65,15 @@ impl FeedbackNoteView {
rollout_path: Option<PathBuf>,
app_event_tx: AppEventSender,
include_logs: bool,
feedback_audience: FeedbackAudience,
) -> Self {
Self {
category,
snapshot,
rollout_path,
app_event_tx,
include_logs,
feedback_audience,
textarea: TextArea::new(),
textarea_state: RefCell::new(TextAreaState::default()),
complete: false,
Expand Down Expand Up @@ -96,30 +111,49 @@ impl FeedbackNoteView {
} else {
"• Feedback recorded (no logs)."
};
let issue_url = issue_url_for_category(self.category, &thread_id);
let issue_url =
issue_url_for_category(self.category, &thread_id, self.feedback_audience);
let mut lines = vec![Line::from(match issue_url.as_ref() {
Some(_) if self.feedback_audience == FeedbackAudience::OpenAiEmployee => {
format!("{prefix} Please report this in #codex-feedback:")
}
Some(_) => format!("{prefix} Please open an issue using the following URL:"),
None => format!("{prefix} Thanks for the feedback!"),
})];
if let Some(url) = issue_url {
lines.extend([
"".into(),
Line::from(vec![" ".into(), url.cyan().underlined()]),
"".into(),
Line::from(vec![
" Or mention your thread ID ".into(),
std::mem::take(&mut thread_id).bold(),
" in an existing issue.".into(),
]),
]);
} else {
lines.extend([
"".into(),
Line::from(vec![
" Thread ID: ".into(),
std::mem::take(&mut thread_id).bold(),
]),
]);
match issue_url {
Some(url) if self.feedback_audience == FeedbackAudience::OpenAiEmployee => {
lines.extend([
"".into(),
Line::from(vec![" ".into(), url.cyan().underlined()]),
"".into(),
Line::from(" Share this and add some info about your problem:"),
Line::from(vec![
" ".into(),
format!("go/codex-feedback/{thread_id}").bold(),
]),
]);
}
Some(url) => {
lines.extend([
"".into(),
Line::from(vec![" ".into(), url.cyan().underlined()]),
"".into(),
Line::from(vec![
" Or mention your thread ID ".into(),
std::mem::take(&mut thread_id).bold(),
" in an existing issue.".into(),
]),
]);
}
None => {
lines.extend([
"".into(),
Line::from(vec![
" Thread ID: ".into(),
std::mem::take(&mut thread_id).bold(),
]),
]);
}
}
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::PlainHistoryCell::new(lines),
Expand Down Expand Up @@ -335,15 +369,35 @@ fn feedback_classification(category: FeedbackCategory) -> &'static str {
}
}

fn issue_url_for_category(category: FeedbackCategory, thread_id: &str) -> Option<String> {
fn issue_url_for_category(
category: FeedbackCategory,
thread_id: &str,
feedback_audience: FeedbackAudience,
) -> Option<String> {
// Only certain categories provide a follow-up link. We intentionally keep
// the external GitHub behavior identical while routing internal users to
// the internal go link.
match category {
FeedbackCategory::Bug | FeedbackCategory::BadResult | FeedbackCategory::Other => Some(
format!("{BASE_BUG_ISSUE_URL}&steps=Uploaded%20thread:%20{thread_id}"),
),
FeedbackCategory::Bug | FeedbackCategory::BadResult | FeedbackCategory::Other => {
Some(match feedback_audience {
FeedbackAudience::OpenAiEmployee => slack_feedback_url(thread_id),
FeedbackAudience::External => {
format!("{BASE_BUG_ISSUE_URL}&steps=Uploaded%20thread:%20{thread_id}")
}
})
}
FeedbackCategory::GoodResult => None,
}
}

/// Build the internal follow-up URL.
///
/// We accept a `thread_id` so the call site stays symmetric with the external
/// path, but we currently point to a fixed channel without prefilling text.
fn slack_feedback_url(_thread_id: &str) -> String {
CODEX_FEEDBACK_INTERNAL_URL.to_string()
}

// Build the selection popup params for feedback categories.
pub(crate) fn feedback_selection_params(
app_event_tx: AppEventSender,
Expand Down Expand Up @@ -523,7 +577,14 @@ mod tests {
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let snapshot = codex_feedback::CodexFeedback::new().snapshot(None);
FeedbackNoteView::new(category, snapshot, None, tx, true)
FeedbackNoteView::new(
category,
snapshot,
None,
tx,
true,
FeedbackAudience::External,
)
}

#[test]
Expand Down Expand Up @@ -556,19 +617,42 @@ mod tests {

#[test]
fn issue_url_available_for_bug_bad_result_and_other() {
let bug_url = issue_url_for_category(FeedbackCategory::Bug, "thread-1");
assert!(
bug_url
.as_deref()
.is_some_and(|url| url.contains("template=2-bug-report"))
let bug_url = issue_url_for_category(
FeedbackCategory::Bug,
"thread-1",
FeedbackAudience::OpenAiEmployee,
);
let expected_slack_url = "http://go/codex-feedback-internal".to_string();
assert_eq!(bug_url.as_deref(), Some(expected_slack_url.as_str()));

let bad_result_url = issue_url_for_category(FeedbackCategory::BadResult, "thread-2");
let bad_result_url = issue_url_for_category(
FeedbackCategory::BadResult,
"thread-2",
FeedbackAudience::OpenAiEmployee,
);
assert!(bad_result_url.is_some());

let other_url = issue_url_for_category(FeedbackCategory::Other, "thread-3");
let other_url = issue_url_for_category(
FeedbackCategory::Other,
"thread-3",
FeedbackAudience::OpenAiEmployee,
);
assert!(other_url.is_some());

assert!(issue_url_for_category(FeedbackCategory::GoodResult, "t").is_none());
assert!(
issue_url_for_category(
FeedbackCategory::GoodResult,
"t",
FeedbackAudience::OpenAiEmployee
)
.is_none()
);
let bug_url_non_employee =
issue_url_for_category(FeedbackCategory::Bug, "t", FeedbackAudience::External);
let expected_external_url = format!("{BASE_BUG_ISSUE_URL}&steps=Uploaded%20thread:%20t");
assert_eq!(
bug_url_non_employee.as_deref(),
Some(expected_external_url.as_str())
);
}
}
1 change: 1 addition & 0 deletions codex-rs/tui/src/bottom_pane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ mod slash_commands;
pub(crate) use footer::CollaborationModeIndicator;
pub(crate) use list_selection_view::SelectionViewParams;
mod feedback_view;
pub(crate) use feedback_view::FeedbackAudience;
pub(crate) use feedback_view::feedback_disabled_params;
pub(crate) use feedback_view::feedback_selection_params;
pub(crate) use feedback_view::feedback_upload_consent_params;
Expand Down
Loading
Loading