Add quick dial presets for terminal title and status line layouts (#54388)

## What changed

Add current terminal title and status line options to the preset picker. Generate `/title` and `/statusline` commands that preserve item order and duplicates, normalize item aliases, and retain the status line color setting. Use `--clear` for empty layouts and disable options containing unavailable items.

## Testing

Extend picker tests and snapshots to cover the new options, generated commands, empty layouts, and unavailable terminal title items.

GitOrigin-RevId: b836b15e87c03249cf479b1c3baf0bd71931a6be
This commit is contained in:
Felipe Coury
2026-10-15 19:13:03 +00:00
committed by copyberry
parent 141bf7680d
commit f847a4bd3c
4 changed files with 134 additions and 11 deletions
+63 -1
View File
@@ -145,6 +145,29 @@ impl ChatWidget {
Ok(dial)
}
pub(super) fn current_terminal_title_quick_dial(&self) -> Result<QuickDial, &'static str> {
status_surface_quick_dial::<TerminalTitleItem>(
"/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<QuickDial, &'static str> {
let colors = if self.local_settings.tui.status_line_use_colors {
"--colors"
} else {
"--no-colors"
};
status_surface_quick_dial::<StatusLineItem>(
"/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<T>(
command: &str,
item_ids: Vec<String>,
suffix: Option<&str>,
invalid_items_error: &'static str,
) -> Result<QuickDial, &'static str>
where
T: std::str::FromStr + ToString,
{
let items = item_ids
.into_iter()
.map(|id| {
id.parse::<T>()
.map(|item| item.to_string())
.map_err(|_| invalid_items_error)
})
.collect::<Result<Vec<_>, _>>()?;
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)
}
@@ -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
@@ -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
@@ -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!(