//! Settings-adjacent popup surfaces for `ChatWidget`.//!//! This keeps theme, personality, and experimental-feature UI out of the main//! orchestration module without changing their event wiring.use super::*;impl ChatWidget { pub(super) fn open_theme_picker(&mut self) { let codex_home = codex_utils_home_dir::find_codex_home().ok(); let terminal_width = self .last_rendered_width .get() .and_then(|width| u16::try_from(width).ok()); let params = crate::theme_picker::build_theme_picker_params( self.config.tui_theme.as_deref(), codex_home.as_deref(), terminal_width, ); self.bottom_pane.show_selection_view(params); } pub(crate) fn open_personality_popup(&mut self) { if !self.is_session_configured() { self.add_info_message( "Personality selection is disabled until startup completes.".to_string(), /*hint*/ None, ); return; } if !self.current_model_supports_personality() { let current_model = self.current_model(); self.add_error_message(format!( "Current model ({current_model}) doesn't support personalities. Try /model to pick a different model." )); return; } self.open_personality_popup_for_current_model(); } fn open_personality_popup_for_current_model(&mut self) { let current_personality = self.config.personality.unwrap_or(Personality::Friendly); let personalities = [Personality::Friendly, Personality::Pragmatic]; let supports_personality = self.current_model_supports_personality(); let items: Vec<SelectionItem> = personalities .into_iter() .map(|personality| { let name = Self::personality_label(personality).to_string(); let description = Some(Self::personality_description(personality).to_string()); let actions: Vec<SelectionAction> = vec![Box::new(move |tx| { tx.send(AppEvent::CodexOp(AppCommand::override_turn_context( /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, /*permission_profile*/ None, /*active_permission_profile*/ None, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, /*summary*/ None, /*service_tier*/ None, /*collaboration_mode*/ None, Some(personality), ))); tx.send(AppEvent::UpdatePersonality(personality)); tx.send(AppEvent::PersistPersonalitySelection { personality }); })]; SelectionItem { name, description, is_current: current_personality == personality, is_disabled: !supports_personality, actions, dismiss_on_select: true, ..Default::default() } }) .collect(); let mut header = ColumnRenderable::new(); header.push(Line::from("Select Personality".bold())); header.push(Line::from("Choose a communication style for Codex.".dim())); self.bottom_pane.show_selection_view(SelectionViewParams { header: Box::new(header), footer_hint: Some(standard_popup_hint_line()), items, ..Default::default() }); } pub(crate) fn open_experimental_popup(&mut self) { let features: Vec<ExperimentalFeatureItem> = FEATURES .iter() .filter_map(|spec| { let name = spec.stage.experimental_menu_name()?; let description = spec.stage.experimental_menu_description()?; Some(ExperimentalFeatureItem { feature: spec.id, name: name.to_string(), description: description.to_string(), enabled: self.config.features.enabled(spec.id), }) }) .collect(); let view = ExperimentalFeaturesView::new( features, self.app_event_tx.clone(), self.bottom_pane.list_keymap(), ); self.bottom_pane.show_view(Box::new(view)); } fn personality_label(personality: Personality) -> &'static str { match personality { Personality::None => "None", Personality::Friendly => "Friendly", Personality::Pragmatic => "Pragmatic", } } fn personality_description(personality: Personality) -> &'static str { match personality { Personality::None => "No personality instructions.", Personality::Friendly => "Warm, collaborative, and helpful.", Personality::Pragmatic => "Concise, task-focused, and direct.", } }}