12 KiB
APP-3709: Auto-Generate Worktree Branch Names — Tech Spec
Problem
generate_worktree_branch_name() in new_worktree_modal.rs uses a global AtomicU32 counter to produce worktree-1, worktree-2, etc. These names are forgettable, collide across sessions, and provide no uniqueness guarantee against existing branches in the target repo. The product spec calls for a themed naming function that combines two random words from a 198-word desert/southwest vocabulary and guarantees uniqueness by escalating to more words on collision.
Relevant Code
warp_util/src/worktree_names.rs— pure naming module (word list,generate_name,generate_unique_name,generate_worktree_branch_name)app/src/util/git.rs:57-78—list_local_branches_sync()(synchronous branch listing)app/src/tab_configs/tab_config.rs:62-91—TabConfigPaneNodestruct (worktree_name_autogenerated)app/src/tab_configs/tab_config.rs:139-174—render_tab_config()(threadsworktree_branch_namethrough to resolution)app/src/tab_configs/tab_config.rs:299-324—resolve_pane_node()leaf branch (template variable substitution)app/src/tab_configs/tab_config.rs:350-403—build_worktree_config_toml()(TOML generation for both manual and autogenerate modes)app/src/workspace/view.rs:7138-7153—maybe_generate_worktree_name()(scans panes, fetches branches, generates name)app/src/workspace/view.rs:7163-7230—handle_new_worktree_submit()(writes TOML, opens config)app/src/workspace/view.rs:4892-4958—open_tab_config_with_params()/open_tab_config()(re-open path)app/src/tab_configs/new_worktree_modal.rs:224-257—try_submit()(emitsNonefor autogenerate,Some(name)for manual)
Current State
Modal flow: When the user clicks "Open" with autogenerate checked, try_submit() calls generate_worktree_branch_name() which returns worktree-{N}. The workspace handler then bakes this name directly into the TOML commands (git worktree add -b worktree-1 ../worktree-1 main) and writes it to ~/.warp/tab_configs/.
Saved config re-open flow: When a saved worktree config is clicked in the menu, open_tab_config() → open_tab_config_with_params() → render_tab_config() → resolve_pane_tree() renders the commands. Currently the branch name is hardcoded in the TOML, so re-opening always tries to create the same branch — which fails if it already exists.
Branch listing: DiffStateModel::get_all_branches() in diff_state.rs runs git for-each-ref --sort=-committerdate --format=%(refname:short) refs/heads asynchronously. The BranchPicker uses this. There is no synchronous branch-listing utility.
Rand usage: The codebase uses rand::seq::SliceRandom with rand::thread_rng() (e.g. agent_tips.rs:467). rand is already a workspace dependency in app/Cargo.toml.
Proposed Changes
1. Pure naming module in warp_util
New file: warp_util/src/worktree_names.rs. This module has zero I/O — it takes a set of existing branch names and returns a unique name. Keeping it in warp_util isolates it from the app crate's compile graph and keeps it trivially testable.
rand needs to be added as a dependency of warp_util (it's already a workspace dep).
Word list: A const WORDS: &[&str] array with the 198 desert/southwest words from the product spec, sorted alphabetically within each category section for readability.
Core function (pure, deterministic with a seeded RNG):
fn generate_name(
word_count: usize,
existing_branches: &HashSet<&str>,
rng: &mut impl rand::Rng,
) -> Option<String>
- Picks
word_countdistinct words at random fromWORDS. - Joins them with
-. - If the result is in
existing_branches, retries (bounded byMAX_RETRIES_PER_LEVEL = 2). - Returns
Some(name)on success,Noneif all retries collided.
Escalating uniqueness wrapper (pure, deterministic with a seeded RNG):
pub fn generate_unique_name(
existing_branches: &HashSet<&str>,
rng: &mut impl rand::Rng,
) -> String
- Starts at
word_count = 2. - Calls
generate_name(word_count, existing_branches, rng). - If
None(all retries collided), incrementsword_countand retries. - Cap at
word_count = 5(198^5 ≈ 2.9 × 10^11 possibilities) as a safety bound, then fall back to appending a random numeric suffix.
Convenience entry point (uses thread_rng):
pub fn generate_worktree_branch_name(
existing_branches: &HashSet<&str>,
) -> String
- Calls
generate_unique_name(existing_branches, &mut rand::thread_rng()). - This is the API call sites use. The
rng-parameterized version exists for deterministic testing.
2. Synchronous git branch listing in app/src/util/git.rs
New function alongside the existing async helpers:
#[cfg(feature = "local_fs")]
pub fn list_local_branches_sync(repo_path: &Path) -> HashSet<String>
- Runs
git branch --list --format=%(refname:short)viastd::process::Command. - Returns a
HashSet<String>of local branch names. - On failure (not a git repo, git not found, etc.) returns an empty set.
Why synchronous: The call sites (handle_new_worktree_submit, maybe_generate_worktree_name in open_tab_config) are synchronous view handlers. git branch --list is a local filesystem read (packed-refs + loose refs) that completes in single-digit milliseconds even for repos with thousands of branches. A synchronous std::process::Command is the simplest approach and avoids threading async through the view layer.
3. Template variable substitution in commands
The commands array on TabConfigPaneNode already exists. The change is in how commands are rendered at open time.
When worktree_branch_name is provided, render_tab_config injects it into both the unquoted and quoted Handlebars context maps under the key autogenerated_branch_name. The existing handlebars::render_template(cmd, quoted) call in resolve_pane_node handles substitution — no custom .replace() needed. This is consistent with how all other tab config params ({{branch}}, {{repo}}, etc.) work.
The commands array in the TOML uses {{autogenerated_branch_name}} as a Handlebars template variable. Users can freely edit the commands — reorder them, add their own (e.g. gt branch create, npm install), or use {{autogenerated_branch_name}} in custom commands. The default commands written by the modal work out-of-the-box without any user editing.
4. Plumbing the generated name through open_tab_config
Modify open_tab_config() in workspace/view.rs: before rendering, scan the config's panes for any with worktree_name_autogenerated = true. If found:
- Read the pane's
cwdto determine the repo path. - Call
list_local_branches_sync(repo_path)to get existing branches. - Call
generate_worktree_branch_name(&existing_branches)to get a fresh name. - Pass the generated name into
render_tab_config(new parameter), which injects it into the Handlebars context so{{autogenerated_branch_name}}in commands gets substituted.
render_tab_config gains an optional worktree_branch_name: Option<&str> parameter. When Some, the name is added to the template context; when None, no extra context is injected. This keeps the API change minimal for all non-worktree configs.
5. TOML generation in handle_new_worktree_submit
Change handle_new_worktree_submit() in workspace/view.rs to write the config with template variables in the commands:
[[panes]]
id = "main"
type = "terminal"
cwd = "/path/to/repo"
worktree_name_autogenerated = true
commands = [
"git worktree add -b {{autogenerated_branch_name}} ../{{autogenerated_branch_name}} main",
"cd ../{{autogenerated_branch_name}}",
]
The commands array uses {{autogenerated_branch_name}} — the same Handlebars syntax as all other tab config params. Users can edit the TOML to add custom commands (e.g. gt branch create, npm install) or reorder them.
6. Update modal and workspace handler call sites
In new_worktree_modal.rs:
- Remove
WORKTREE_COUNTERand the oldgenerate_worktree_branch_name(). try_submit()emitsworktree_branch_name: Nonewhen autogenerate is on,Some(name)when the user typed a name manually. The modal does not generate names — it delegates that to the workspace handler.
In workspace/view.rs:
handle_new_worktree_submit()receivesworktree_branch_name: Option<&str>. WhenNone, it callslist_local_branches_syncthengenerate_worktree_branch_nameto produce a fresh name. This name is used both as the TOML filename hint and as the branch name for the initial open.maybe_generate_worktree_name()is a shared helper used by bothopen_tab_config()(re-open path) andhandle_tab_config_params_modal_body_event()(params modal submit). It scans panes forworktree_name_autogenerated = true, fetches branches, and generates a name.
7. Module registration
Add pub mod worktree_names; to warp_util/src/lib.rs.
End-to-End Flow
Modal flow (new worktree)
- User clicks "Open" with autogenerate checked.
try_submit()emitsSubmit { repo, branch: "main", worktree_branch_name: None }.handle_new_worktree_submit()receivesNone, callslist_local_branches_sync(repo_path)to get existing branches.- Passes the branch set to
generate_worktree_branch_name(&branches). Naming function generates a 2-word name; on collision, retries at the same word count then escalates. - Returns e.g.
mesa-coyote. - Workspace handler writes TOML with
worktree_name_autogenerated = trueandcommandscontaining{{autogenerated_branch_name}}template variables viabuild_worktree_config_toml. - Parses the TOML back into a
TabConfigand callsopen_tab_config_with_paramsdirectly with the just-generated name — no second generation pass.
Re-open flow (saved config)
- User clicks a saved worktree config in the menu.
open_tab_config()scans panes, findsworktree_name_autogenerated = true.- Reads
cwdfrom the pane to determine the repo path. - Calls
list_local_branches_sync(repo_path)→HashSet. - Calls
generate_worktree_branch_name(&branches)→ e.g.obsidian-monsoon. - Passes the name into
render_tab_config(..., Some("obsidian-monsoon")). render_tab_configinjectsobsidian-monsooninto the Handlebars context, andresolve_pane_noderenders{{autogenerated_branch_name}}→obsidian-monsoonin all commands via normal Handlebars substitution.- New tab opens with the substituted commands.
Risks and Mitigations
- Synchronous git call on main thread:
git branch --listis a local filesystem read (packed-refs + loose refs). Completes in single-digit milliseconds even for repos with thousands of branches. If this ever becomes a concern, the function can be made async without changing the API surface. - Word list staleness: The 198-word list is a compile-time constant. No runtime mechanism to update it. This is intentional — the list is a curated vocabulary, not a growing dictionary.
Testing and Validation
- Unit tests in
warp_util/src/worktree_names.rs:generate_namewith a seeded RNG produces deterministic output.- Generated names match
{word}-{word}format, both words are inWORDS, words are distinct. generate_unique_nameavoids all names in a providedexisting_branchesset.- Escalation: pre-fill
existing_brancheswith enough 2-word combos to force a 3-word name, verify output has 3 words. - All words in
WORDSare valid git branch name components (no spaces, no.., no control chars, no leading-).
- Unit test for template substitution: Verify that a pane with
worktree_name_autogenerated = trueand commands containing{{autogenerated_branch_name}}produces the expected substituted commands. - Unit test for custom commands: Verify that user-added commands (with and without
{{autogenerated_branch_name}}) are preserved and substituted correctly. cargo check: Verify no compilation errors after all changes.
Follow-ups
- Add a test that exercises the synchronous
git branch --listpath against a real temporary git repo. - Consider supporting a
worktree_path_templatefield on the pane for users who want worktrees in a non-default location (e.g.~/worktrees/{name}instead of../{name}).