8.7 KiB
APP-3679: New Worktree Modal — Tech Spec
Problem
The "Create new tab config..." / "New worktree" menu items open a raw TOML template in an editor. This requires users to understand the TOML schema to create worktree configs. The new worktree modal provides a GUI-based "worktree factory" that generates, persists, and opens worktree tab configs without manual TOML editing.
Relevant Code
app/src/tab_configs/new_worktree_modal.rs— new modal body view (created in this change)app/src/tab_configs/mod.rs— module registrationapp/src/tab_configs/params_modal.rs— reference pattern for modal body viewsapp/src/tab_configs/repo_picker.rs— reusedRepoPickercomponentapp/src/tab_configs/branch_picker.rs— reusedBranchPickercomponent (includes no-commit fallback and loading placeholder)app/src/view_components/filterable_dropdown.rs—FilterableDropdown(stale selection fix inset_items)app/src/workspace/view.rs (4636-4690, 4776-4816)— menu item definitions for horizontal and vertical tabsapp/src/workspace/view.rs (7057-7196)— event handlers and TOML generationapp/src/workspace/view.rs (1447-1476)—build_new_worktree_modalapp/src/workspace/action.rs:554-557—OpenNewWorktreeModalandOpenNewWorktreeRepoPickeraction variantsapp/src/workspace/util.rs:123—is_new_worktree_modal_openworkspace state flagapp/src/user_config/mod.rs:188-209—find_unused_worktree_config_pathutilityapp/src/modal.rs—Modal<T>andModalViewState<T>wrappersui/src/ui_components/checkbox.rs—Checkboxcomponent
Current State
The horizontal tab menu's last item is "Create new tab config..." which calls create_and_open_new_tab_config — writes a TOML template to ~/.warp/tab_configs/ and opens it in the user's editor. The vertical tab menu has the same action under the label "New worktree".
Existing infrastructure:
RepoPicker: filterable dropdown of known repos fromPersistedWorkspace, with "+ Add new repo..." footer.BranchPicker: filterable dropdown of git branches for a repo, with async fetch and main-branch sorting.Modal<T>/ModalViewState<T>: standard modal pattern used byTabConfigParamsModal.- Tab config filesystem watcher in
user_config/native.rsautomatically reloads~/.warp/tab_configs/on file changes.
Proposed Changes
1. New view: NewWorktreeModal
File: app/src/tab_configs/new_worktree_modal.rs
A View + TypedActionView body for use inside Modal<NewWorktreeModal>:
- State:
RepoPicker,BranchPicker,autogenerate_branch_name: bool,selected_repo/branch: Option<String>, mouse states for checkbox/cancel/open buttons. - Events:
NewWorktreeModalEvent::{ Close, Submit { repo, branch, autogenerate_name }, PickNewRepo }. - Actions:
NewWorktreeModalAction::{ Cancel, Open, ToggleAutogenerate }. - Render: Column layout with repo picker, branch picker, checkbox row, and footer bar with Cancel/Open buttons. Body padding handled by the modal body view (not the
Modalwrapper, which haspadding: 0). on_open: Rebuilds pickers fresh, resets checkbox to checked, focuses the repo picker.- Repo → Branch sync: When
RepoPickerEvent::Selectedfires, callsbranch_picker.refetch_branches(path). generate_worktree_branch_name(): StaticAtomicU32counter producingworktree-1,worktree-2, etc.
BranchPicker improvements
- Loading placeholder: When a branch fetch starts, the dropdown shows "Fetching branches…" as a selected placeholder item (inside the dropdown top bar) rather than as a separate text element below the dropdown. This prevents the modal from shifting layout.
selected_value()returnsNonewhileis_loadingis true so the placeholder is never treated as a real branch selection. - No-commit repo fallback:
git for-each-ref refs/headsonly lists refs backed by actual commits, so a freshly initialised repo (git init, no commits) returns an empty branch list. Thefetch_branchesasync block detects this and falls back todetect_current_branch(which usesgit branch --show-current) to populate the picker with the initial branch (e.g. "main"). - FilterableDropdown stale selection fix:
FilterableDropdown::set_itemsnow clears the cachedselected_itemwhen the old selection's label is absent from the replacement item list. Previously, callingset_items(vec![])left a staleselected_itemthat caused the dropdown top bar to show a ghost label andselected_item_label()to return a value for a non-existent item.
2. Workspace wiring
Follows the exact same pattern as tab_config_params_modal:
- Field:
new_worktree_modal: ModalViewState<Modal<NewWorktreeModal>>onWorkspace. - Builder:
build_new_worktree_modal(ctx)— creates body, subscribes to body and modal events, wraps inModalwith compact header, 460×400px size, zero body padding. - Action:
OpenNewWorktreeModal— gets CWD from active terminal, callsbody.on_open(cwd), opens the modal. - Action:
OpenNewWorktreeRepoPicker— opens a folder picker, registers the new path inPersistedWorkspace, updates the modal's repo picker. - Render:
if self.new_worktree_modal.is_open() { stack.add_child(...) }in the overlay section.
3. Menu updates
Both new_session_menu_items (horizontal) and vertical_tabs_new_session_menu_items (vertical) replace their last "tab config creation" item with "+ New Worktree" → WorkspaceAction::OpenNewWorktreeModal with icons::Icon::Plus.
4. TOML generation on submit
handle_new_worktree_submit(repo, branch, autogenerate_name):
- Determine branch name: use
generate_worktree_branch_name()if autogenerate, else use the selected branch (fallback to generated name if none). - Build TOML string with
name = "Worktree: {branch_name}", single[[panes]]withtype = "terminal",cwd,worktree_name_autogenerated, andcommands = ["git worktree add ...", "cd ..."]. - Write to
~/.warp/tab_configs/worktree_{sanitized_branch}.tomlusingfind_unused_worktree_config_path. - Parse the TOML back into a
TabConfigand callopen_tab_config(config, ctx). - The filesystem watcher picks up the new file and makes it available in the menu.
5. Utility: find_unused_worktree_config_path
In user_config/mod.rs. Sanitizes branch name for filename safety (alphanumeric, -, _ only), tries worktree_{name}.toml, then worktree_{name}_1.toml, etc.
End-to-End Flow
- User clicks
+ New Worktreein either tab menu. WorkspaceAction::OpenNewWorktreeModaldispatched.- Workspace calls
body.on_open(cwd), opens theModalViewState. - User interacts with repo/branch pickers and checkbox.
- User clicks "Open" →
NewWorktreeModalAction::Open→try_submit()→ emitsNewWorktreeModalEvent::Submit. - Workspace receives
Submitevent →handle_new_worktree_submit()generates TOML, writes file, parses it, callsopen_tab_config(). - New tab opens with the worktree commands. File watcher detects the new
.tomland adds it toWarpConfig.tab_configs. - Modal closes.
Risks and Mitigations
- TOML format drift: The generated TOML must match what
TabConfig::deserializeexpects. Mitigation: the TOML is immediately parsed back after writing; any mismatch is caught and logged. - Branch name sanitization: Special characters in branch names could produce invalid filenames. Mitigation:
find_unused_worktree_config_pathsanitizes to alphanumeric +-+_. - Counter reset:
WORKTREE_COUNTERis a staticAtomicU32that resets on app restart, so branch names may collide across sessions. Mitigation: the TOML content includes the full branch name so collisions only affect filenames (handled by the_Nsuffix).
Testing and Validation
- Build check:
cargo check -p warppasses with no errors or warnings. - Manual testing: Open both horizontal and vertical tab menus, verify "+ New Worktree" appears, opens the modal, and the full flow works.
- Filesystem: Verify
.tomlfiles appear in~/.warp/tab_configs/with correct content. - Re-use: Verify saved worktree configs appear in the menu and can be re-opened.
Follow-ups
- Replace the placeholder
generate_worktree_branch_name()with a more sophisticated naming scheme (e.g., based on date, repo name, or user initials). - Add validation that the selected repo is a valid git repository before allowing submission.
- Consider adding a text input for manual branch name entry when autogenerate is unchecked.
- Explore support for worktree deletion or management from the menu.
- Consider showing a distinct empty state in the branch picker when the repo genuinely has no branches (i.e. the
detect_current_branchfallback also fails).