9.1 KiB
APP-3830: Worktree Sidecar Selection Semantics — Tech Spec
Related product spec: specs/moirahuang/APP-3830/PRODUCT.md
Related prior feature spec: specs/moirahuang/APP-3743/TECH.md
Problem
The worktree sidecar sits in an awkward spot between generic menu behavior and Workspace-owned worktree creation.
Before this change:
- repo rows in the sidecar relied on direct typed-action dispatch from the sidecar overlay
- the sidecar tracked
selected_row_indexandhovered_row_indexseparately - keyboard initialization selected the first repo row, but mouse hover only updated
hovered_row_index MenuEvent::ItemSelectedrepresented selection movement, not execution
That combination caused two classes of bugs:
- execution could fail because the sidecar overlay dispatched
WorkspaceAction::OpenWorktreeInRepofrom a context where no view handled it - the visually hovered repo could differ from the underlying actionable selection, so click and keyboard confirmation could resolve different rows
Relevant code
app/src/menu.rs (52-125)— genericMenu<A>state, including the newdispatch_item_actionsflagapp/src/menu.rs (1253-1451)—MenuItem::renderplumbing that now threadsdispatch_item_actionsthrough click handlingapp/src/menu.rs (1747-1989)—Menu::new()default behavior andwithout_item_action_dispatch()app/src/menu.rs (2208-2359)—SubMenu::handle_action()andTypedActionView for Menu<A>app/src/workspace/view.rs (715-746)—WorkspaceMenuHandlesandNewSessionSidecarSelectionapp/src/workspace/view.rs (978-1071)— worktree sidecar selection helpers, keyboard confirm path, and hover-to-selection syncapp/src/workspace/view.rs (1588-1638)— sidecar menu construction inbuild_menus()app/src/workspace/view.rs (7430-7484)—handle_new_session_sidecar_event()app/src/workspace/view.rs (7489-7694)— worktree sidecar item construction, search row, repo rows, and pinned footerapp/src/workspace/view.rs (8124-8180)—open_worktree_in_repo()app/src/workspace/view_test.rs (258-389)— sidecar hover precedence and execution-path regression tests
Current state
Menu dispatch model
Menu<A> historically assumed that clicking or pressing Enter on a row should dispatch the row's typed action directly from the menu view. That works for menus whose handlers live in the same responder chain, but the worktree sidecar is an overlay whose repo-opening behavior belongs to Workspace.
Sidecar state model
The sidecar menu stores:
selected_row_indexfor keyboard-driven selectionhovered_row_indexfor mouse hover
The search field requires an initial keyboard selection so Up, Down, and Enter can operate from the focused editor. But once the mouse moves over a different repo row, the hovered row became visually prominent without necessarily becoming the actionable selection.
Why not execute from ItemSelected
MenuEvent::ItemSelected is emitted whenever the menu selection changes, including:
- arrow-key movement
- programmatic selection initialization
- click-driven row selection before close
If Workspace executed repo opening on every ItemSelected, arrow-key navigation would incorrectly open worktrees while the user was only moving through the list.
Proposed changes
1. Add an opt-in non-dispatch mode to Menu
Extend Menu<A> with a dispatch_item_actions: bool flag that defaults to true.
Add:
Menu::without_item_action_dispatch()
When disabled:
- click still dispatches
MenuAction::Select(...) - click still dispatches
MenuAction::Close(true) Enterstill emitsItemSelectedandClose(true)- the menu does not dispatch the row's typed action directly
This keeps existing menus unchanged while giving overlay-style menus a way to separate "selection changed" from "execute action."
2. Make the sidecar menu local to Workspace
Change the sidecar from Menu<WorkspaceAction> to Menu<NewSessionSidecarSelection>.
NewSessionSidecarSelection carries only local sidecar intent:
OpenWorktreeRepo { repo_path }- Windows-only terminal variants for the Terminal sidecar path
This removes the repo-row dependency on responder-chain dispatch and makes Workspace the sole owner of repo execution.
3. Keep execution in Workspace
Add helper methods on Workspace:
selected_new_session_sidecar_selection()execute_new_session_sidecar_selection()confirm_worktree_sidecar_selection()
Execution semantics become:
- mouse click path: resolve the selected sidecar item during
MenuEvent::Close { via_select_item: true } - keyboard path from the search editor:
confirm_worktree_sidecar_selection()resolves and executes directly inWorkspace
This preserves keyboard behavior without relying on generic menu Enter execution.
4. Make hover update actionable selection
Add sync_new_session_sidecar_selection_to_hover().
On MenuEvent::ItemHovered, Workspace:
- reads
hovered_index() - checks whether the hovered row is actionable via
MenuItem::item_on_select_action() - promotes the hovered row to
selected_index()if it differs from the current selection
This is intentionally limited to actionable rows so that the search row does not replace repo selection just because the mouse passed over it.
5. Preserve footer correctness
The pinned + Add new repo footer still dispatches WorkspaceAction::OpenWorktreeAddRepoPicker directly from its click closure.
To avoid executing a stale repo selection on the subsequent close event, OpenWorktreeAddRepoPicker first calls close_new_session_dropdown_menu(ctx), which clears sidecar selection state before opening the folder picker.
6. Keep config creation out of the routing fix
open_worktree_in_repo() and ensure_default_worktree_config() remain the execution endpoint for the worktree flow. This PR does not change default-config semantics; it only makes sure the correct repo selection reliably reaches that endpoint.
The logging added around default-config creation and worktree opening improves diagnosis when failures are due to config IO or parsing rather than sidecar routing.
End-to-end flow
sequenceDiagram
participant User
participant SearchEditor
participant SidecarMenu
participant Workspace
participant WorktreeFlow
User->>Workspace: Open "New worktree config"
Workspace->>SidecarMenu: Populate repo rows and select first actionable repo
User->>SearchEditor: Type query / press Up or Down
SearchEditor->>Workspace: EditorEvent
Workspace->>SidecarMenu: Rebuild items or update selection
User->>SidecarMenu: Hover different repo row
SidecarMenu->>Workspace: MenuEvent::ItemHovered
Workspace->>SidecarMenu: Sync hovered row into selected row
alt Mouse click on repo row
User->>SidecarMenu: Click repo
SidecarMenu->>Workspace: MenuEvent::Close { via_select_item: true }
Workspace->>Workspace: Resolve selected sidecar item
Workspace->>WorktreeFlow: open_worktree_in_repo(repo_path)
else Keyboard Enter from search editor
User->>SearchEditor: Press Enter
SearchEditor->>Workspace: EditorEvent::Enter
Workspace->>Workspace: Resolve selected sidecar item
Workspace->>WorktreeFlow: open_worktree_in_repo(repo_path)
end
Risks and mitigations
Risk: generic menu behavior changes regress unrelated menus
Mitigation:
dispatch_item_actionsdefaults totrue- only the new-session sidecar opts into
without_item_action_dispatch()
Risk: hover accidentally selects non-action rows
Mitigation:
- hover-to-selection sync only runs when
MenuItem::item_on_select_action()is present - the search row remains non-actionable
Risk: footer clicks execute stale repo state
Mitigation:
OpenWorktreeAddRepoPickerclears the dropdown and sidecar state before opening the folder picker
Risk: mouse and keyboard paths diverge again
Mitigation:
- both paths now resolve through the same sidecar-local selection model
- regression tests cover click-like close, keyboard enter, and hover precedence
Testing and validation
Automated validation should cover:
test_worktree_sidecar_hover_takes_precedence_over_selectiontest_worktree_sidecar_close_via_select_item_executes_from_workspacetest_worktree_sidecar_search_editor_enter_executes_selection- existing search-editor navigation and escape behavior in
test_worktree_sidecar_search_editor_proxies_navigation_and_escape
Manual validation should verify:
- hovering a repo row updates the active selection highlight
- clicking the hovered row opens the correct repo worktree
- the
OpenWorktreeInRepo ... was dispatched, but no view handled itwarning no longer appears for repo-row selection + Add new repoopens only the picker
Follow-ups
- If more sidecar-style menus need
Workspace-owned execution, consider formalizing a generic "selection without direct action dispatch" pattern beyond this localized flag. - If menu execution semantics keep growing more complex, a future cleanup could introduce a distinct executed/confirmed event instead of overloading selection and close events.