From 24a96d60c9dce4b4a8c4b8d6c583af756ee76263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Soares?= <37777652+Dnreikronos@users.noreply.github.com> Date: Tue, 28 Apr 2026 19:03:56 -0300 Subject: [PATCH] Sort tab configs alphabetically by name (#9258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #9132 Description Tab configs loaded from ~/.warp/tab_configs/ appeared in non-deterministic order in the + menu and the default session mode dropdown in settings. The root cause is WalkDir returning directory entries in filesystem order, which varies across platforms and runs. Added a case-insensitive alphabetical sort by name in load_tab_configs() (app/src/user_config/native.rs), so all consumers receive a stable, predictable ordering without needing per-site sorting. Testing - cargo check -p warp passes with no errors. - No new tests added. The change is a single sort_by call on an existing Vec — the sorting behavior is deterministic and trivially correct. Manual verification: create multiple .toml files in ~/.warp/tab_configs/ with names like "Zebra", "alpha", "Beta" and confirm they appear as alpha, Beta, Zebra in the + menu. Agent Mode - Warp Agent Mode - This PR was created via Warp's AI Agent Mode Changelog Entries for Stable CHANGELOG-BUG-FIX: Tab configs in the + menu and default session mode dropdown are now sorted alphabetically by name instead of appearing in random order. --- app/src/user_config/mod.rs | 2 ++ app/src/user_config/mod_test.rs | 62 +++++++++++++++++++++++++++++++++ app/src/user_config/native.rs | 5 +++ 3 files changed, 69 insertions(+) diff --git a/app/src/user_config/mod.rs b/app/src/user_config/mod.rs index 0075bfa1..74efc952 100644 --- a/app/src/user_config/mod.rs +++ b/app/src/user_config/mod.rs @@ -17,6 +17,8 @@ use std::path::PathBuf; use warp_core::ui::theme::WarpTheme; use warpui::{Entity, ModelContext, SingletonEntity}; +#[cfg(test)] +pub(crate) use imp::load_tab_configs; #[cfg(feature = "local_fs")] pub use imp::load_workflows; pub use imp::{load_launch_configs, load_theme_configs}; diff --git a/app/src/user_config/mod_test.rs b/app/src/user_config/mod_test.rs index f47f69a8..022bc087 100644 --- a/app/src/user_config/mod_test.rs +++ b/app/src/user_config/mod_test.rs @@ -1,5 +1,6 @@ use super::*; use std::collections::HashMap; +use std::io::Write; use std::path::Path; use crate::launch_configs::launch_config::PaneTemplateType; @@ -112,3 +113,64 @@ fn test_sanitize_toml_base_name_replaces_spaces_and_dots() { fn test_sanitize_toml_base_name_falls_back_for_empty_result() { assert_eq!(sanitize_toml_base_name("..."), "worktree"); } + +fn write_tab_config_toml(dir: &Path, file_name: &str, config_name: &str) { + let path = dir.join(file_name); + let mut f = std::fs::File::create(path).unwrap(); + write!(f, "name = \"{}\"", config_name).unwrap(); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_sorts_case_insensitive() { + let dir = tempfile::tempdir().unwrap(); + write_tab_config_toml(dir.path(), "zebra.toml", "Zebra"); + write_tab_config_toml(dir.path(), "alpha.toml", "alpha"); + write_tab_config_toml(dir.path(), "beta.toml", "Beta"); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + let names: Vec<&str> = configs.iter().map(|c| c.name.as_str()).collect(); + assert_eq!(names, vec!["alpha", "Beta", "Zebra"]); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_deterministic_tie_breaking() { + let dir = tempfile::tempdir().unwrap(); + write_tab_config_toml(dir.path(), "upper.toml", "Alpha"); + write_tab_config_toml(dir.path(), "lower.toml", "alpha"); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + let names: Vec<&str> = configs.iter().map(|c| c.name.as_str()).collect(); + assert_eq!(names, vec!["Alpha", "alpha"]); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_empty_directory() { + let dir = tempfile::tempdir().unwrap(); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(configs.is_empty()); + assert!(errors.is_empty()); +} + +#[cfg(feature = "local_fs")] +#[test] +fn test_load_tab_configs_skips_non_toml_files() { + let dir = tempfile::tempdir().unwrap(); + write_tab_config_toml(dir.path(), "real.toml", "Real"); + std::fs::write(dir.path().join("readme.md"), "not a config").unwrap(); + std::fs::write(dir.path().join("data.json"), "{}").unwrap(); + + let (configs, errors) = load_tab_configs(dir.path()); + + assert!(errors.is_empty()); + assert_eq!(configs.len(), 1); + assert_eq!(configs[0].name, "Real"); +} diff --git a/app/src/user_config/native.rs b/app/src/user_config/native.rs index 1ab988dc..8ca82291 100644 --- a/app/src/user_config/native.rs +++ b/app/src/user_config/native.rs @@ -204,6 +204,11 @@ pub fn load_tab_configs(tab_config_path: &Path) -> (Vec, Vec errors.push(error), } } + configs.sort_by(|a, b| { + let a_name = a.name.to_lowercase(); + let b_name = b.name.to_lowercase(); + a_name.cmp(&b_name).then_with(|| a.name.cmp(&b.name)) + }); (configs, errors) }