diff --git a/codex-rs/tui/src/chatwidget/quick_dials.rs b/codex-rs/tui/src/chatwidget/quick_dials.rs index 0414299f7d..cf702a39d2 100644 --- a/codex-rs/tui/src/chatwidget/quick_dials.rs +++ b/codex-rs/tui/src/chatwidget/quick_dials.rs @@ -145,6 +145,29 @@ impl ChatWidget { Ok(dial) } + pub(super) fn current_terminal_title_quick_dial(&self) -> Result { + status_surface_quick_dial::( + "/title", + self.configured_terminal_title_items(), + /*suffix*/ None, + "Choose available terminal-title items with /title before saving.", + ) + } + + pub(super) fn current_status_line_quick_dial(&self) -> Result { + let colors = if self.local_settings.tui.status_line_use_colors { + "--colors" + } else { + "--no-colors" + }; + status_surface_quick_dial::( + "/statusline", + self.configured_status_line_items(), + /*suffix*/ Some(colors), + "Choose available status-line items with /statusline before saving.", + ) + } + fn quick_dials_available(&mut self) -> bool { if self.blocks_direct_input || self.active_side_conversation @@ -384,9 +407,13 @@ impl ChatWidget { .unwrap_or(Err( "Select a named permission profile with /permissions first.", )); + let terminal_title = self.current_terminal_title_quick_dial(); + let status_line = self.current_status_line_quick_dial(); let items = [ ("Current model", self.current_model_quick_dial()), ("Current permissions", permissions), + ("Current terminal title", terminal_title), + ("Current status line", status_line), ] .into_iter() .map(|(name, dial)| { @@ -415,7 +442,7 @@ impl ChatWidget { .collect(); self.bottom_pane.show_selection_view(SelectionViewParams { view_id: Some("quick-dial-prefills"), - max_visible_rows: 2, + max_visible_rows: 4, title: Some("Start preset from current settings".into()), items, ..SelectionViewParams::picker() @@ -566,3 +593,38 @@ impl ChatWidget { consumed } } + +fn status_surface_quick_dial( + command: &str, + item_ids: Vec, + suffix: Option<&str>, + invalid_items_error: &'static str, +) -> Result +where + T: std::str::FromStr + ToString, +{ + let items = item_ids + .into_iter() + .map(|id| { + id.parse::() + .map(|item| item.to_string()) + .map_err(|_| invalid_items_error) + }) + .collect::, _>>()?; + let mut input = command.to_string(); + if items.is_empty() { + input.push_str(" --clear"); + } else { + for item in items { + input.push(' '); + input.push_str(&item); + } + } + if let Some(suffix) = suffix { + input.push(' '); + input.push_str(suffix); + } + let dial = QuickDial { input, label: None }; + dial.validate()?; + Ok(dial) +} diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source.snap index c07269a2f4..e07988ea75 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source.snap @@ -5,8 +5,10 @@ expression: "render_bottom_popup(&chat, 80)" Start preset from current settings -› 1. Current model /model gpt-5.5 --speed=standard - 2. Current permissions /permissions :workspace --approval-policy=on-request - --reviewer=user +› 1. Current model /model gpt-5.5 --speed=standard + 2. Current permissions /permissions :workspace --approval-policy=on- + request --reviewer=user + 3. Current terminal title /title model project-name model + 4. Current status line /statusline model project-name model --no-colors enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source_unavailable_model.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source_unavailable_model.snap index 5e0df09238..638c80dd59 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source_unavailable_model.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__saved_quick_dials_source_unavailable_model.snap @@ -5,9 +5,12 @@ expression: "render_bottom_popup(&chat, 80)" Start preset from current settings - Current model Choose an available reasoning effort with /model - before saving. -› 1. Current permissions /permissions :workspace --approval-policy=on-request - --reviewer=auto_review + Current model Choose an available reasoning effort with /model + before saving. +› 1. Current permissions /permissions :workspace --approval-policy=on- + request --reviewer=auto_review + Current terminal title Choose available terminal-title items with /title + before saving. + 2. Current status line /statusline model --colors enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/tests/quick_dials_tests.rs b/codex-rs/tui/src/chatwidget/tests/quick_dials_tests.rs index 23e278edbf..c53b1f5075 100644 --- a/codex-rs/tui/src/chatwidget/tests/quick_dials_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/quick_dials_tests.rs @@ -5,10 +5,21 @@ use crate::chatwidget::quick_dials::QuickDialEditorTarget; use codex_config::types::QuickDial; #[tokio::test] -async fn current_settings_offer_model_and_permissions() { +async fn current_settings_offer_model_permissions_and_status_layouts() { let (mut chat, mut events, _ops) = make_chatwidget_manual(/*model_override*/ Some("gpt-5.5")).await; chat.thread_id = Some(ThreadId::new()); + chat.local_settings.tui.terminal_title = Some(vec![ + "model-name".into(), + "project".into(), + "model-name".into(), + ]); + chat.local_settings.tui.status_line = Some(vec![ + "model-name".into(), + "project-root".into(), + "model-name".into(), + ]); + chat.local_settings.tui.status_line_use_colors = false; chat.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active( PermissionProfile::workspace_write(), ActivePermissionProfile::new(":workspace"), @@ -31,14 +42,60 @@ async fn current_settings_offer_model_and_permissions() { }) if input == "/permissions :workspace --approval-policy=on-request --reviewer=user" ); + chat.open_quick_dial_prefills(); + chat.handle_key_event(KeyCode::Down.into()); + chat.handle_key_event(KeyCode::Down.into()); + chat.handle_key_event(KeyCode::Enter.into()); + assert_matches!( + events.try_recv(), + Ok(AppEvent::OpenQuickDialEditor { + target: QuickDialEditorTarget::PickSlot, + dial: Some(QuickDial { input, label: None }), + }) if input == "/title model project-name model" + ); + + chat.open_quick_dial_prefills(); + chat.handle_key_event(KeyCode::Down.into()); + chat.handle_key_event(KeyCode::Down.into()); + chat.handle_key_event(KeyCode::Down.into()); + chat.handle_key_event(KeyCode::Enter.into()); + assert_matches!( + events.try_recv(), + Ok(AppEvent::OpenQuickDialEditor { + target: QuickDialEditorTarget::PickSlot, + dial: Some(QuickDial { input, label: None }), + }) if input == "/statusline model project-name model --no-colors" + ); + + chat.local_settings.tui.terminal_title = Some(Vec::new()); + chat.local_settings.tui.status_line = Some(Vec::new()); + chat.local_settings.tui.status_line_use_colors = true; + assert_eq!( + chat.current_terminal_title_quick_dial(), + Ok(QuickDial { + input: "/title --clear".into(), + label: None, + }) + ); + assert_eq!( + chat.current_status_line_quick_dial(), + Ok(QuickDial { + input: "/statusline --clear --colors".into(), + label: None, + }) + ); + chat.set_reasoning_effort(Some(ReasoningEffortConfig::Custom("removed-effort".into()))); + chat.terminal_title_invalid_items_warned + .store(true, std::sync::atomic::Ordering::Relaxed); + chat.local_settings.tui.terminal_title = Some(vec!["removed-title-item".into()]); + chat.local_settings.tui.status_line = Some(vec!["model".into()]); chat.config.approvals_reviewer = ApprovalsReviewer::AutoReview; chat.open_quick_dial_prefills(); assert_chatwidget_snapshot!( "saved_quick_dials_source_unavailable_model", render_bottom_popup(&chat, /*width*/ 80) ); - chat.handle_key_event(KeyCode::Down.into()); chat.handle_key_event(KeyCode::Enter.into()); assert_matches!( events.try_recv(), @@ -67,7 +124,6 @@ async fn current_settings_offer_model_and_permissions() { )) .expect("set named permission profile"); chat.open_quick_dial_prefills(); - chat.handle_key_event(KeyCode::Down.into()); chat.handle_key_event(KeyCode::Enter.into()); let policy = serde_json::to_string(&granular).expect("granular policy serializes"); assert_matches!(