Move typewriter names to branch_names.rs with full database

Richard Feldman created

Extract the typewriter name list and branch name generation logic into
a dedicated branch_names.rs module. Replace the hand-picked 49-name
list with 838 names sourced from typewriterdatabase.com, filtered to
exclude parenthesized variants, names with special characters, and
non-ASCII entries.

Change summary

crates/agent_ui/src/agent_panel.rs  | 149 -----
crates/agent_ui/src/agent_ui.rs     |   1 
crates/agent_ui/src/branch_names.rs | 769 +++++++++++++++++++++++++++++++
3 files changed, 771 insertions(+), 148 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -52,7 +52,6 @@ use assistant_slash_command::SlashCommandWorkingSet;
 use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
 use client::UserStore;
 use cloud_api_types::Plan;
-use collections::HashSet;
 use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
 use extension::ExtensionEvents;
 use extension_host::ExtensionStore;
@@ -68,7 +67,6 @@ use language_model::{ConfigurationError, LanguageModelRegistry};
 use project::project_settings::ProjectSettings;
 use project::{Project, ProjectPath, Worktree};
 use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
-use rand::Rng;
 use rules_library::{RulesLibrary, open_rules_library};
 use search::{BufferSearchBar, buffer_search};
 use settings::{Settings, update_settings_file};
@@ -93,96 +91,6 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
 const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
 const DEFAULT_THREAD_TITLE: &str = "New Thread";
 
-const TYPEWRITER_NAMES: &[&str] = &[
-    "adler",
-    "blick",
-    "caligraph",
-    "clipper",
-    "consul",
-    "continental",
-    "coronet",
-    "corsair",
-    "densmore",
-    "dora",
-    "electra",
-    "erika",
-    "everest",
-    "facit",
-    "galaxie",
-    "groma",
-    "halda",
-    "hammond",
-    "hansen",
-    "hermes",
-    "imperial",
-    "kolibri",
-    "lettera",
-    "lexikon",
-    "monarch",
-    "monica",
-    "nakajima",
-    "noiseless",
-    "olivetti",
-    "olympia",
-    "optima",
-    "pluma",
-    "praxis",
-    "remington",
-    "robotron",
-    "royal",
-    "selectric",
-    "skyriter",
-    "splendid",
-    "sterling",
-    "studio",
-    "tippa",
-    "torpedo",
-    "traveller",
-    "triumph",
-    "underwood",
-    "valentine",
-    "voss",
-    "woodstock",
-];
-
-fn pick_typewriter_name(existing_branches: &[&str], rng: &mut impl Rng) -> Option<&'static str> {
-    let disallowed: HashSet<&str> = existing_branches
-        .iter()
-        .filter_map(|branch| branch.rsplit_once('-').map(|(prefix, _)| prefix))
-        .collect();
-
-    let available: Vec<&'static str> = TYPEWRITER_NAMES
-        .iter()
-        .copied()
-        .filter(|name| !disallowed.contains(name))
-        .collect();
-
-    if available.is_empty() {
-        return None;
-    }
-
-    let index = rng.random_range(0..available.len());
-    Some(available[index])
-}
-
-fn generate_agent_branch_name_from_rng(
-    existing_branches: &[&str],
-    rng: &mut impl Rng,
-) -> Option<String> {
-    let typewriter_name = pick_typewriter_name(existing_branches, rng)?;
-    let hash: String = (0..8)
-        .map(|_| {
-            let idx: u8 = rng.random_range(0..36);
-            if idx < 10 {
-                (b'0' + idx) as char
-            } else {
-                (b'a' + idx - 10) as char
-            }
-        })
-        .collect();
-    Some(format!("{typewriter_name}-{hash}"))
-}
-
 fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
     let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
     let key = i64::from(workspace_id).to_string();
@@ -2135,7 +2043,7 @@ impl AgentPanel {
 
     fn generate_agent_branch_name() -> String {
         let mut rng = rand::rng();
-        generate_agent_branch_name_from_rng(&[], &mut rng)
+        crate::branch_names::generate_branch_name(&[], &mut rng)
             .expect("should always succeed with no disallowed names")
     }
 
@@ -4386,64 +4294,9 @@ mod tests {
     use fs::FakeFs;
     use gpui::{TestAppContext, VisualTestContext};
     use project::Project;
-    use rand::rngs::StdRng;
     use serde_json::json;
     use workspace::MultiWorkspace;
 
-    #[gpui::test(iterations = 10)]
-    fn test_pick_typewriter_name_with_no_disallowed(mut rng: StdRng) {
-        let name = pick_typewriter_name(&[], &mut rng);
-        assert!(name.is_some());
-        assert!(TYPEWRITER_NAMES.contains(&name.unwrap()));
-    }
-
-    #[gpui::test(iterations = 10)]
-    fn test_pick_typewriter_name_excludes_taken_names(mut rng: StdRng) {
-        let branch_names = &["olivetti-abc12345", "selectric-def67890"];
-        let name = pick_typewriter_name(branch_names, &mut rng).unwrap();
-        assert_ne!(name, "olivetti");
-        assert_ne!(name, "selectric");
-    }
-
-    #[gpui::test]
-    fn test_pick_typewriter_name_all_taken(mut rng: StdRng) {
-        let branch_names: Vec<String> = TYPEWRITER_NAMES
-            .iter()
-            .map(|name| format!("{name}-00000000"))
-            .collect();
-        let branch_name_refs: Vec<&str> = branch_names.iter().map(|s| s.as_str()).collect();
-        let name = pick_typewriter_name(&branch_name_refs, &mut rng);
-        assert!(name.is_none());
-    }
-
-    #[gpui::test(iterations = 10)]
-    fn test_pick_typewriter_name_ignores_branches_without_hyphen(mut rng: StdRng) {
-        let branch_names = &["main", "develop", "feature"];
-        let name = pick_typewriter_name(branch_names, &mut rng);
-        assert!(name.is_some());
-        assert!(TYPEWRITER_NAMES.contains(&name.unwrap()));
-    }
-
-    #[gpui::test(iterations = 10)]
-    fn test_generate_agent_branch_name_format(mut rng: StdRng) {
-        let branch_name = generate_agent_branch_name_from_rng(&[], &mut rng).unwrap();
-        let (prefix, suffix) = branch_name.rsplit_once('-').unwrap();
-        assert!(TYPEWRITER_NAMES.contains(&prefix));
-        assert_eq!(suffix.len(), 8);
-        assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
-    }
-
-    #[gpui::test]
-    fn test_generate_agent_branch_name_returns_none_when_exhausted(mut rng: StdRng) {
-        let branch_names: Vec<String> = TYPEWRITER_NAMES
-            .iter()
-            .map(|name| format!("{name}-00000000"))
-            .collect();
-        let branch_name_refs: Vec<&str> = branch_names.iter().map(|s| s.as_str()).collect();
-        let result = generate_agent_branch_name_from_rng(&branch_name_refs, &mut rng);
-        assert!(result.is_none());
-    }
-
     #[gpui::test]
     async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
         init_test(cx);

crates/agent_ui/src/agent_ui.rs 🔗

@@ -3,6 +3,7 @@ mod agent_diff;
 mod agent_model_selector;
 mod agent_panel;
 mod agent_registry_ui;
+mod branch_names;
 mod buffer_codegen;
 mod completion_provider;
 mod config_options;

crates/agent_ui/src/branch_names.rs 🔗

@@ -0,0 +1,769 @@
+use collections::HashSet;
+use rand::Rng;
+
+/// Names of historical typewriter brands, for use in auto-generated branch names.
+/// (Hyphens and parens have been dropped so that the branch names are one-word.)
+///
+/// Thanks to https://typewriterdatabase.com/alph.0.brands for the names!
+const TYPEWRITER_NAMES: &[&str] = &[
+    "abeille",
+    "acme",
+    "addo",
+    "adler",
+    "adlerette",
+    "adlerita",
+    "admiral",
+    "agamli",
+    "agar",
+    "agidel",
+    "agil",
+    "aguia",
+    "aguila",
+    "aigle",
+    "ahram",
+    "ajax",
+    "aktiv",
+    "ala",
+    "alba",
+    "albus",
+    "alexander",
+    "alexis",
+    "alfa",
+    "allen",
+    "alonso",
+    "alpina",
+    "amata",
+    "amaya",
+    "amka",
+    "anavi",
+    "anderson",
+    "andina",
+    "antares",
+    "apex",
+    "apsco",
+    "aquila",
+    "archo",
+    "ardita",
+    "argyle",
+    "aristocrat",
+    "aristokrat",
+    "arlington",
+    "armstrong",
+    "arpha",
+    "artus",
+    "astoria",
+    "atlantia",
+    "atlantic",
+    "atlas",
+    "augusta",
+    "aurora",
+    "austro",
+    "automatic",
+    "avanti",
+    "avona",
+    "azzurra",
+    "bajnok",
+    "baldwin",
+    "balkan",
+    "baltica",
+    "baltimore",
+    "barlock",
+    "barr",
+    "barrat",
+    "bartholomew",
+    "bashkiriya",
+    "bavaria",
+    "beaucourt",
+    "beko",
+    "belka",
+    "bennett",
+    "bennington",
+    "berni",
+    "bianca",
+    "bijou",
+    "bing",
+    "bisei",
+    "biser",
+    "bluebird",
+    "bolida",
+    "borgo",
+    "boston",
+    "boyce",
+    "bradford",
+    "brandenburg",
+    "brigitte",
+    "briton",
+    "brooks",
+    "brosette",
+    "buddy",
+    "burns",
+    "burroughs",
+    "byron",
+    "calanda",
+    "caligraph",
+    "cappel",
+    "cardinal",
+    "carissima",
+    "carlem",
+    "carlton",
+    "carmen",
+    "cawena",
+    "cella",
+    "celtic",
+    "century",
+    "champignon",
+    "cherryland",
+    "chevron",
+    "chicago",
+    "cicero",
+    "cifra",
+    "citizen",
+    "claudia",
+    "cleveland",
+    "clover",
+    "coffman",
+    "cole",
+    "columbia",
+    "commercial",
+    "companion",
+    "concentra",
+    "concord",
+    "concordia",
+    "conover",
+    "constanta",
+    "consul",
+    "conta",
+    "contenta",
+    "contimat",
+    "contina",
+    "continento",
+    "cornelia",
+    "coronado",
+    "cosmopolita",
+    "courier",
+    "craftamatic",
+    "crandall",
+    "crown",
+    "culema",
+    "dactyle",
+    "dankers",
+    "dart",
+    "daugherty",
+    "davis",
+    "dayton",
+    "depantio",
+    "dea",
+    "delmar",
+    "densmore",
+    "diadema",
+    "dial",
+    "diamant",
+    "diana",
+    "dictatype",
+    "diplomat",
+    "diskret",
+    "dolfus",
+    "dollar",
+    "domus",
+    "drake",
+    "draper",
+    "duplex",
+    "durabel",
+    "dynacord",
+    "eagle",
+    "eclipse",
+    "edelmann",
+    "edelweiss",
+    "edison",
+    "edita",
+    "edland",
+    "efka",
+    "eldorado",
+    "electa",
+    "electromatic",
+    "elektro",
+    "elgin",
+    "elliot",
+    "emerson",
+    "emka",
+    "emona",
+    "empire",
+    "engadine",
+    "engler",
+    "erfurt",
+    "erika",
+    "esko",
+    "essex",
+    "eureka",
+    "europa",
+    "everest",
+    "everlux",
+    "excelsior",
+    "express",
+    "fabers",
+    "facit",
+    "fairbanks",
+    "faktotum",
+    "famos",
+    "federal",
+    "felio",
+    "fidat",
+    "filius",
+    "fips",
+    "fish",
+    "fitch",
+    "fleet",
+    "florida",
+    "flott",
+    "flyer",
+    "flying",
+    "fontana",
+    "ford",
+    "forto",
+    "fortuna",
+    "fox",
+    "framo",
+    "franconia",
+    "franklin",
+    "friden",
+    "frolio",
+    "furstenberg",
+    "galesburg",
+    "galiette",
+    "gallia",
+    "garbell",
+    "gardner",
+    "geka",
+    "generation",
+    "genia",
+    "geniatus",
+    "gerda",
+    "gisela",
+    "glashutte",
+    "gloria",
+    "godrej",
+    "gossen",
+    "gourland",
+    "grandjean",
+    "granta",
+    "granville",
+    "graphic",
+    "gritzner",
+    "groma",
+    "guhl",
+    "guidonia",
+    "gundka",
+    "hacabo",
+    "haddad",
+    "halberg",
+    "halda",
+    "hall",
+    "hammond",
+    "hammonia",
+    "harmony",
+    "hanford",
+    "hansa",
+    "harris",
+    "hartford",
+    "hassia",
+    "hatch",
+    "heady",
+    "hebronia",
+    "hebros",
+    "hega",
+    "helios",
+    "helma",
+    "herald",
+    "hercules",
+    "hermes",
+    "herold",
+    "heros",
+    "hesperia",
+    "hogar",
+    "hooven",
+    "hopkins",
+    "horton",
+    "hugin",
+    "hungaria",
+    "hurtu",
+    "iberia",
+    "idea",
+    "ideal",
+    "imperia",
+    "impo",
+    "industria",
+    "industrio",
+    "ingersoll",
+    "international",
+    "invicta",
+    "irene",
+    "iris",
+    "iskra",
+    "ivitsa",
+    "ivriah",
+    "jackson",
+    "janalif",
+    "janos",
+    "jolux",
+    "juki",
+    "junior",
+    "juventa",
+    "juwel",
+    "kamkap",
+    "kamo",
+    "kanzler",
+    "kappel",
+    "karli",
+    "karstadt",
+    "keaton",
+    "kenbar",
+    "keystone",
+    "kim",
+    "klein",
+    "kneist",
+    "knoch",
+    "koh",
+    "kolibri",
+    "kolumbus",
+    "komet",
+    "kondor",
+    "koniger",
+    "konryu",
+    "kontor",
+    "kosmopolit",
+    "krypton",
+    "lambert",
+    "lasalle",
+    "lectra",
+    "leframa",
+    "lemair",
+    "lemco",
+    "liberty",
+    "libia",
+    "liga",
+    "lignose",
+    "lilliput",
+    "lindeteves",
+    "linowriter",
+    "listvitsa",
+    "ludolf",
+    "lutece",
+    "luxa",
+    "lyubava",
+    "mafra",
+    "magnavox",
+    "maher",
+    "majestic",
+    "majitouch",
+    "manhattan",
+    "mapuua",
+    "marathon",
+    "marburger",
+    "maritsa",
+    "maruzen",
+    "maskelyne",
+    "masspro",
+    "matous",
+    "mccall",
+    "mccool",
+    "mcloughlin",
+    "mead",
+    "mechno",
+    "mehano",
+    "meiselbach",
+    "melbi",
+    "melior",
+    "melotyp",
+    "mentor",
+    "mepas",
+    "mercedesia",
+    "mercurius",
+    "mercury",
+    "merkur",
+    "merritt",
+    "merz",
+    "messa",
+    "meteco",
+    "meteor",
+    "micron",
+    "mignon",
+    "mikro",
+    "minerva",
+    "mirian",
+    "mirina",
+    "mitex",
+    "molle",
+    "monac",
+    "monarch",
+    "mondiale",
+    "monica",
+    "monofix",
+    "monopol",
+    "monpti",
+    "monta",
+    "montana",
+    "montgomery",
+    "moon",
+    "morgan",
+    "morris",
+    "morse",
+    "moya",
+    "moyer",
+    "munson",
+    "musicwriter",
+    "nadex",
+    "nakajima",
+    "neckermann",
+    "neubert",
+    "neya",
+    "ninety",
+    "nisa",
+    "noiseless",
+    "noor",
+    "nora",
+    "nord",
+    "norden",
+    "norica",
+    "norma",
+    "norman",
+    "north",
+    "nototyp",
+    "nova",
+    "novalevi",
+    "odell",
+    "odhner",
+    "odo",
+    "odoma",
+    "ohio",
+    "ohtani",
+    "oliva",
+    "oliver",
+    "olivetti",
+    "olympia",
+    "omega",
+    "optima",
+    "orbis",
+    "orel",
+    "orga",
+    "oriette",
+    "orion",
+    "orn",
+    "orplid",
+    "pacior",
+    "pagina",
+    "parisienne",
+    "passat",
+    "pearl",
+    "peerless",
+    "perfect",
+    "perfecta",
+    "perkeo",
+    "perkins",
+    "perlita",
+    "pettypet",
+    "phoenix",
+    "piccola",
+    "picht",
+    "pinnock",
+    "pionier",
+    "plurotyp",
+    "plutarch",
+    "pneumatic",
+    "pocket",
+    "polyglott",
+    "polygraph",
+    "pontiac",
+    "portable",
+    "portex",
+    "pozzi",
+    "premier",
+    "presto",
+    "primavera",
+    "progress",
+    "protos",
+    "pterotype",
+    "pullman",
+    "pulsatta",
+    "quick",
+    "racer",
+    "rand",
+    "radio",
+    "rally",
+    "readers",
+    "reed",
+    "referent",
+    "reff",
+    "regent",
+    "regia",
+    "regina",
+    "rekord",
+    "reliable",
+    "reliance",
+    "remsho",
+    "remagg",
+    "rembrandt",
+    "remer",
+    "remington",
+    "remstar",
+    "remtor",
+    "reporters",
+    "resko",
+    "rex",
+    "rexpel",
+    "rheinita",
+    "rheinmetall",
+    "rival",
+    "roberts",
+    "robotron",
+    "rocher",
+    "rochester",
+    "roebuck",
+    "rofa",
+    "roland",
+    "rooy",
+    "rover",
+    "roxy",
+    "roy",
+    "royal",
+    "rundstatler",
+    "sabaudia",
+    "sabb",
+    "saleem",
+    "salter",
+    "sampo",
+    "sarafan",
+    "saturn",
+    "saxonia",
+    "schade",
+    "schapiro",
+    "schreibi",
+    "scripta",
+    "sears",
+    "secor",
+    "selectric",
+    "selekta",
+    "senator",
+    "sense",
+    "senta",
+    "serd",
+    "shilling",
+    "shimade",
+    "shimer",
+    "sholes",
+    "shuang",
+    "siegfried",
+    "siemag",
+    "silma",
+    "silver",
+    "simplex",
+    "simtype",
+    "singer",
+    "smith",
+    "soemtron",
+    "sonja",
+    "speedwriter",
+    "sphinx",
+    "starlet",
+    "stearns",
+    "steel",
+    "stella",
+    "steno",
+    "sterling",
+    "stoewer",
+    "stolzenberg",
+    "stott",
+    "strangfeld",
+    "sture",
+    "stylotyp",
+    "sun",
+    "superba",
+    "superia",
+    "supermetall",
+    "surety",
+    "swintec",
+    "swissa",
+    "talbos",
+    "talleres",
+    "tatrapoint",
+    "taurus",
+    "taylorix",
+    "tell",
+    "tempotype",
+    "society",
+    "tippco",
+    "titania",
+    "tops",
+    "towa",
+    "toyo",
+    "tradition",
+    "transatlantic",
+    "traveller",
+    "trebla",
+    "triumph",
+    "turia",
+    "typatune",
+    "typen",
+    "typorium",
+    "ugro",
+    "ultima",
+    "unda",
+    "underwood",
+    "unica",
+    "unitype",
+    "ursula",
+    "utax",
+    "varityper",
+    "vasanta",
+    "vendex",
+    "venus",
+    "victor",
+    "victoria",
+    "video",
+    "viking",
+    "vira",
+    "virotyp",
+    "visigraph",
+    "vittoria",
+    "volcan",
+    "vornado",
+    "voss",
+    "vultur",
+    "waltons",
+    "wanamaker",
+    "wanderer",
+    "ward",
+    "warner",
+    "waterloo",
+    "waverley",
+    "wayne",
+    "webster",
+    "wedgefield",
+    "welco",
+    "wellington",
+    "wellon",
+    "weltblick",
+    "westphalia",
+    "wiedmer",
+    "williams",
+    "wilson",
+    "winkel",
+    "winsor",
+    "wizard",
+    "woodstock",
+    "woodwards",
+    "yatran",
+    "yost",
+    "zenit",
+    "zentronik",
+    "zeta",
+    "zeya",
+];
+
+/// Picks a typewriter name that isn't already taken by an existing branch.
+///
+/// Each entry in `existing_branches` is expected to be a full branch name
+/// like `"olivetti-a3f9b2c1"`. The prefix before the last `'-'` is treated
+/// as the taken typewriter name. Branches without a `'-'` are ignored.
+///
+/// Returns `None` when every name in the pool is already taken.
+pub fn pick_typewriter_name(
+    existing_branches: &[&str],
+    rng: &mut impl Rng,
+) -> Option<&'static str> {
+    let disallowed: HashSet<&str> = existing_branches
+        .iter()
+        .filter_map(|branch| branch.rsplit_once('-').map(|(prefix, _)| prefix))
+        .collect();
+
+    let available: Vec<&'static str> = TYPEWRITER_NAMES
+        .iter()
+        .copied()
+        .filter(|name| !disallowed.contains(name))
+        .collect();
+
+    if available.is_empty() {
+        return None;
+    }
+
+    let index = rng.random_range(0..available.len());
+    Some(available[index])
+}
+
+/// Generates a branch name like `"olivetti-a3f9b2c1"` by picking a typewriter
+/// name that isn't already taken and appending an 8-character alphanumeric hash.
+///
+/// Returns `None` when every typewriter name in the pool is already taken.
+pub fn generate_branch_name(existing_branches: &[&str], rng: &mut impl Rng) -> Option<String> {
+    let typewriter_name = pick_typewriter_name(existing_branches, rng)?;
+    let hash: String = (0..8)
+        .map(|_| {
+            let idx: u8 = rng.random_range(0..36);
+            if idx < 10 {
+                (b'0' + idx) as char
+            } else {
+                (b'a' + idx - 10) as char
+            }
+        })
+        .collect();
+    Some(format!("{typewriter_name}-{hash}"))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use rand::rngs::StdRng;
+
+    #[gpui::test(iterations = 10)]
+    fn test_pick_typewriter_name_with_no_disallowed(mut rng: StdRng) {
+        let name = pick_typewriter_name(&[], &mut rng);
+        assert!(name.is_some());
+        assert!(TYPEWRITER_NAMES.contains(&name.unwrap()));
+    }
+
+    #[gpui::test(iterations = 10)]
+    fn test_pick_typewriter_name_excludes_taken_names(mut rng: StdRng) {
+        let branch_names = &["olivetti-abc12345", "selectric-def67890"];
+        let name = pick_typewriter_name(branch_names, &mut rng).unwrap();
+        assert_ne!(name, "olivetti");
+        assert_ne!(name, "selectric");
+    }
+
+    #[gpui::test]
+    fn test_pick_typewriter_name_all_taken(mut rng: StdRng) {
+        let branch_names: Vec<String> = TYPEWRITER_NAMES
+            .iter()
+            .map(|name| format!("{name}-00000000"))
+            .collect();
+        let branch_name_refs: Vec<&str> = branch_names.iter().map(|s| s.as_str()).collect();
+        let name = pick_typewriter_name(&branch_name_refs, &mut rng);
+        assert!(name.is_none());
+    }
+
+    #[gpui::test(iterations = 10)]
+    fn test_pick_typewriter_name_ignores_branches_without_hyphen(mut rng: StdRng) {
+        let branch_names = &["main", "develop", "feature"];
+        let name = pick_typewriter_name(branch_names, &mut rng);
+        assert!(name.is_some());
+        assert!(TYPEWRITER_NAMES.contains(&name.unwrap()));
+    }
+
+    #[gpui::test(iterations = 10)]
+    fn test_generate_branch_name_format(mut rng: StdRng) {
+        let branch_name = generate_branch_name(&[], &mut rng).unwrap();
+        let (prefix, suffix) = branch_name.rsplit_once('-').unwrap();
+        assert!(TYPEWRITER_NAMES.contains(&prefix));
+        assert_eq!(suffix.len(), 8);
+        assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
+    }
+
+    #[gpui::test]
+    fn test_generate_branch_name_returns_none_when_exhausted(mut rng: StdRng) {
+        let branch_names: Vec<String> = TYPEWRITER_NAMES
+            .iter()
+            .map(|name| format!("{name}-00000000"))
+            .collect();
+        let branch_name_refs: Vec<&str> = branch_names.iter().map(|s| s.as_str()).collect();
+        let result = generate_branch_name(&branch_name_refs, &mut rng);
+        assert!(result.is_none());
+    }
+}