diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 92a1a67f8..7b13a7f28 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -3678,6 +3678,16 @@ impl CodexMessageProcessor { } async fn upload_feedback(&self, request_id: RequestId, params: FeedbackUploadParams) { + if !self.config.feedback_enabled { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "sending feedback is disabled by configuration".to_string(), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + let FeedbackUploadParams { classification, reason, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 162c3226e..668d205cc 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -356,6 +356,10 @@ pub struct Config { /// Defaults to `true`. pub analytics: bool, + /// When `false`, disables feedback collection across Codex product surfaces. + /// Defaults to `true`. + pub feedback_enabled: bool, + /// OTEL configuration (exporter type, endpoint, headers, etc.). pub otel: crate::config::types::OtelConfig, } @@ -820,6 +824,10 @@ pub struct ConfigToml { /// Defaults to `true`. pub analytics: Option, + /// When `false`, disables feedback collection across Codex product surfaces. + /// Defaults to `true`. + pub feedback: Option, + /// OTEL configuration. pub otel: Option, @@ -1403,6 +1411,11 @@ impl Config { .and_then(|a| a.enabled) .or(cfg.analytics.as_ref().and_then(|a| a.enabled)) .unwrap_or(true), + feedback_enabled: cfg + .feedback + .as_ref() + .and_then(|feedback| feedback.enabled) + .unwrap_or(true), tui_notifications: cfg .tui .as_ref() @@ -1559,6 +1572,7 @@ mod tests { use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; use crate::config::edit::apply_blocking; + use crate::config::types::FeedbackConfigToml; use crate::config::types::HistoryPersistence; use crate::config::types::McpServerTransportConfig; use crate::config::types::Notifications; @@ -1885,6 +1899,25 @@ trust_level = "trusted" Ok(()) } + #[test] + fn feedback_enabled_defaults_to_true() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cfg = ConfigToml { + feedback: Some(FeedbackConfigToml::default()), + ..Default::default() + }; + + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides::default(), + codex_home.path().to_path_buf(), + )?; + + assert_eq!(config.feedback_enabled, true); + + Ok(()) + } + #[test] fn profile_legacy_toggles_override_base() -> std::io::Result<()> { let codex_home = TempDir::new()?; @@ -3234,6 +3267,7 @@ model_verbosity = "high" animations: true, show_tooltips: true, analytics: true, + feedback_enabled: true, tui_scroll_events_per_tick: None, tui_scroll_wheel_lines: None, tui_scroll_trackpad_lines: None, @@ -3318,6 +3352,7 @@ model_verbosity = "high" animations: true, show_tooltips: true, analytics: true, + feedback_enabled: true, tui_scroll_events_per_tick: None, tui_scroll_wheel_lines: None, tui_scroll_trackpad_lines: None, @@ -3417,6 +3452,7 @@ model_verbosity = "high" animations: true, show_tooltips: true, analytics: false, + feedback_enabled: true, tui_scroll_events_per_tick: None, tui_scroll_wheel_lines: None, tui_scroll_trackpad_lines: None, @@ -3502,6 +3538,7 @@ model_verbosity = "high" animations: true, show_tooltips: true, analytics: true, + feedback_enabled: true, tui_scroll_events_per_tick: None, tui_scroll_wheel_lines: None, tui_scroll_trackpad_lines: None, diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index f0d31f7c9..657bb052a 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -282,6 +282,12 @@ pub struct AnalyticsConfigToml { pub enabled: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct FeedbackConfigToml { + /// When `false`, disables the feedback flow across Codex product surfaces. + pub enabled: Option, +} + // ===== OTEL configuration ===== #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index c563ab8e9..8fef8e79a 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -380,6 +380,21 @@ pub(crate) fn feedback_selection_params( } } +/// Build the selection popup params shown when feedback is disabled. +pub(crate) fn feedback_disabled_params() -> super::SelectionViewParams { + super::SelectionViewParams { + title: Some("Sending feedback is disabled".to_string()), + subtitle: Some("This action is disabled by configuration.".to_string()), + footer_hint: Some(standard_popup_hint_line()), + items: vec![super::SelectionItem { + name: "Close".to_string(), + dismiss_on_select: true, + ..Default::default() + }], + ..Default::default() + } +} + fn make_feedback_item( app_event_tx: AppEventSender, name: &str, diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index fe626537a..857959786 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -34,6 +34,7 @@ mod prompt_args; mod skill_popup; pub(crate) use list_selection_view::SelectionViewParams; mod feedback_view; +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; mod paste_burst; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index bd0ba788b..02283386d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1703,6 +1703,12 @@ impl ChatWidget { } match cmd { SlashCommand::Feedback => { + if !self.config.feedback_enabled { + let params = crate::bottom_pane::feedback_disabled_params(); + self.bottom_pane.show_selection_view(params); + self.request_redraw(); + return; + } // Step 1: pick a category (UI built in feedback_view) let params = crate::bottom_pane::feedback_selection_params(self.app_event_tx.clone()); diff --git a/codex-rs/tui2/src/bottom_pane/feedback_view.rs b/codex-rs/tui2/src/bottom_pane/feedback_view.rs index c563ab8e9..8fef8e79a 100644 --- a/codex-rs/tui2/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui2/src/bottom_pane/feedback_view.rs @@ -380,6 +380,21 @@ pub(crate) fn feedback_selection_params( } } +/// Build the selection popup params shown when feedback is disabled. +pub(crate) fn feedback_disabled_params() -> super::SelectionViewParams { + super::SelectionViewParams { + title: Some("Sending feedback is disabled".to_string()), + subtitle: Some("This action is disabled by configuration.".to_string()), + footer_hint: Some(standard_popup_hint_line()), + items: vec![super::SelectionItem { + name: "Close".to_string(), + dismiss_on_select: true, + ..Default::default() + }], + ..Default::default() + } +} + fn make_feedback_item( app_event_tx: AppEventSender, name: &str, diff --git a/codex-rs/tui2/src/bottom_pane/mod.rs b/codex-rs/tui2/src/bottom_pane/mod.rs index 4b6caf0d1..8258d6b50 100644 --- a/codex-rs/tui2/src/bottom_pane/mod.rs +++ b/codex-rs/tui2/src/bottom_pane/mod.rs @@ -32,6 +32,7 @@ mod prompt_args; mod skill_popup; pub(crate) use list_selection_view::SelectionViewParams; mod feedback_view; +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; mod paste_burst; diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index df5acb442..06b281e7b 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -1537,6 +1537,12 @@ impl ChatWidget { } match cmd { SlashCommand::Feedback => { + if !self.config.feedback_enabled { + let params = crate::bottom_pane::feedback_disabled_params(); + self.bottom_pane.show_selection_view(params); + self.request_redraw(); + return; + } // Step 1: pick a category (UI built in feedback_view) let params = crate::bottom_pane::feedback_selection_params(self.app_event_tx.clone());