# 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` — `TabConfigPaneNode` struct (`worktree_name_autogenerated`) - `app/src/tab_configs/tab_config.rs:139-174` — `render_tab_config()` (threads `worktree_branch_name` through 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()` (emits `None` for 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): ```rust fn generate_name( word_count: usize, existing_branches: &HashSet<&str>, rng: &mut impl rand::Rng, ) -> Option ``` - Picks `word_count` distinct words at random from `WORDS`. - Joins them with `-`. - If the result is in `existing_branches`, retries (bounded by `MAX_RETRIES_PER_LEVEL = 2`). - Returns `Some(name)` on success, `None` if all retries collided. **Escalating uniqueness wrapper** (pure, deterministic with a seeded RNG): ```rust 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), increments `word_count` and 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): ```rust 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: ```rust #[cfg(feature = "local_fs")] pub fn list_local_branches_sync(repo_path: &Path) -> HashSet ``` - Runs `git branch --list --format=%(refname:short)` via `std::process::Command`. - Returns a `HashSet` 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: 1. Read the pane's `cwd` to determine the repo path. 2. Call `list_local_branches_sync(repo_path)` to get existing branches. 3. Call `generate_worktree_branch_name(&existing_branches)` to get a fresh name. 4. 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: ```toml [[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_COUNTER` and the old `generate_worktree_branch_name()`. - `try_submit()` emits `worktree_branch_name: None` when 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()` receives `worktree_branch_name: Option<&str>`. When `None`, it calls `list_local_branches_sync` then `generate_worktree_branch_name` to 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 both `open_tab_config()` (re-open path) and `handle_tab_config_params_modal_body_event()` (params modal submit). It scans panes for `worktree_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) 1. User clicks "Open" with autogenerate checked. 2. `try_submit()` emits `Submit { repo, branch: "main", worktree_branch_name: None }`. 3. `handle_new_worktree_submit()` receives `None`, calls `list_local_branches_sync(repo_path)` to get existing branches. 4. 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. 5. Returns e.g. `mesa-coyote`. 6. Workspace handler writes TOML with `worktree_name_autogenerated = true` and `commands` containing `{{autogenerated_branch_name}}` template variables via `build_worktree_config_toml`. 7. Parses the TOML back into a `TabConfig` and calls `open_tab_config_with_params` directly with the just-generated name — no second generation pass. ### Re-open flow (saved config) 1. User clicks a saved worktree config in the menu. 2. `open_tab_config()` scans panes, finds `worktree_name_autogenerated = true`. 3. Reads `cwd` from the pane to determine the repo path. 4. Calls `list_local_branches_sync(repo_path)` → `HashSet`. 5. Calls `generate_worktree_branch_name(&branches)` → e.g. `obsidian-monsoon`. 6. Passes the name into `render_tab_config(..., Some("obsidian-monsoon"))`. 7. `render_tab_config` injects `obsidian-monsoon` into the Handlebars context, and `resolve_pane_node` renders `{{autogenerated_branch_name}}` → `obsidian-monsoon` in all commands via normal Handlebars substitution. 8. New tab opens with the substituted commands. ## Risks and Mitigations - **Synchronous git call on main thread**: `git branch --list` is 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_name` with a seeded RNG produces deterministic output. - Generated names match `{word}-{word}` format, both words are in `WORDS`, words are distinct. - `generate_unique_name` avoids all names in a provided `existing_branches` set. - Escalation: pre-fill `existing_branches` with enough 2-word combos to force a 3-word name, verify output has 3 words. - All words in `WORDS` are 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 = true` and 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 --list` path against a real temporary git repo. - Consider supporting a `worktree_path_template` field on the pane for users who want worktrees in a non-default location (e.g. `~/worktrees/{name}` instead of `../{name}`).