feat(task-modal): add project selection via number keys#1390
feat(task-modal): add project selection via number keys#1390drochag wants to merge 2 commits intogeneralaction:mainfrom
Conversation
Add the ability to select a project using number keys (1-9) when the TaskModal is open via Cmd+N. Previously, tasks were always created in the currently active project with no way to change it from the modal. Changes: - Add inline project selector to TaskModal header showing all projects with number badges (1-9) - Allow number key selection when no input field is focused - Pre-select the current project by default - Update branch options when project changes - Pass selected project to handleCreateTask Closes generalaction#1376 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@drochag is attempting to deploy a commit to the General Action Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR adds a project-selector UI to the Key changes and issues found:
Confidence Score: 2/5
|
| Filename | Overview |
|---|---|
| src/renderer/components/TaskModal.tsx | Adds inline project selector with number-key shortcuts and local branch loading. Contains a race condition in concurrent branch-load calls when the user switches projects quickly. |
| src/renderer/hooks/useTaskManagement.ts | Adds optional project parameter to handleCreateTask and uses it as the primary target. Missing setSelectedProject call when creating a task for a non-active project, leaving UI in inconsistent state. |
Sequence Diagram
sequenceDiagram
participant User
participant TaskModal
participant ProjectManagementContext
participant useTaskManagement
participant ElectronAPI
User->>TaskModal: Opens modal (Cmd+N)
TaskModal->>ProjectManagementContext: reads projects, selectedProject, contextBranchOptions
TaskModal->>TaskModal: useState(selectedModalProject = selectedProject)
User->>TaskModal: Presses number key OR clicks project
TaskModal->>TaskModal: setSelectedModalProject(projects[index])
alt selectedModalProject === selectedProject (context project)
TaskModal->>TaskModal: uses contextBranchOptions
else different project selected
TaskModal->>ElectronAPI: listRemoteBranches / sshExecuteCommand
ElectronAPI-->>TaskModal: branch list
TaskModal->>TaskModal: setLocalBranchOptions(...)
end
User->>TaskModal: Submits form
TaskModal->>useTaskManagement: handleCreateTask(..., selectedModalProject)
useTaskManagement->>useTaskManagement: targetProject = project (selectedModalProject)
Note over useTaskManagement: ⚠️ setSelectedProject NOT called if project ≠ selectedProject
useTaskManagement->>useTaskManagement: createTaskMutation.mutateAsync(targetProject)
useTaskManagement->>useTaskManagement: setActiveTask(optimisticTask) for targetProject
Note over useTaskManagement: selectedProject still points to old project → inconsistent state
Comments Outside Diff (1)
-
src/renderer/hooks/useTaskManagement.ts, line 906-941 (link)Missing project navigation when creating task for a non-active project
When a task is created via the new project selector for a project that isn't the currently active one,
handleCreateTaskcorrectly uses the passedprojectargument astargetProject, but neitherhandleCreateTasknorcreateTaskMutation.onMutate/onSuccesscallssetSelectedProject(targetProject).As a result, the app ends up in an inconsistent state:
activeTask.projectIdpoints to the newly created task's project (B)selectedProjectremains the previously active project (A)
This means the sidebar still shows project A's task list while the main view attempts to render a task belonging to project B. Compare this to the existing sidebar flow (
handleStartCreateTaskFromSidebar), which explicitly callsactivateProjectView(targetProject)before opening the modal to pre-switch the project.A
setSelectedProject(targetProject)call (or equivalent navigation) should be made inhandleCreateTaskwhen the providedprojectdiffers fromselectedProject:const targetProject = project || pendingTaskProjectRef.current || selectedProject; pendingTaskProjectRef.current = null; if (!targetProject) return; // Navigate to the target project if it differs from the currently selected one if (targetProject.id !== selectedProject?.id) { setSelectedProject(targetProject); } setIsCreatingTask(true); // ...
Last reviewed commit: 5657f80
| const loadBranches = async () => { | ||
| setLocalIsLoadingBranches(true); | ||
| const initialBranch = selectedModalProject.gitInfo?.baseRef || 'main'; | ||
| setLocalBranchOptions([{ value: initialBranch, label: initialBranch }]); | ||
|
|
||
| try { | ||
| let options: { value: string; label: string }[]; | ||
|
|
||
| if (selectedModalProject.isRemote && selectedModalProject.sshConnectionId) { | ||
| const result = await window.electronAPI.sshExecuteCommand( | ||
| selectedModalProject.sshConnectionId, | ||
| 'git branch -a --format="%(refname:short)"', | ||
| selectedModalProject.path | ||
| ); | ||
| if (result.exitCode === 0 && result.stdout) { | ||
| const branches = result.stdout | ||
| .split('\n') | ||
| .map((b) => b.trim()) | ||
| .filter((b) => b.length > 0 && !b.includes('HEAD')); | ||
| options = branches.map((b) => ({ value: b, label: b })); | ||
| } else { | ||
| options = []; | ||
| } | ||
| } else { | ||
| const res = await window.electronAPI.listRemoteBranches({ | ||
| projectPath: selectedModalProject.path, | ||
| }); | ||
| if (res.success && res.branches) { | ||
| options = res.branches.map((b) => ({ | ||
| value: b.ref, | ||
| label: b.remote ? b.label : `${b.branch} (local)`, | ||
| })); | ||
| } else { | ||
| options = []; | ||
| } | ||
| } | ||
|
|
||
| if (options.length > 0) { | ||
| setLocalBranchOptions(options); | ||
| } | ||
| } catch (error) { | ||
| console.error('Failed to load branches:', error); | ||
| } finally { | ||
| setLocalIsLoadingBranches(false); | ||
| } | ||
| }; | ||
|
|
||
| userChangedBranchRef.current = false; | ||
| setSelectedBranch(selectedModalProject.gitInfo?.baseRef || 'main'); | ||
| void loadBranches(); |
There was a problem hiding this comment.
Race condition in branch loading
When a user rapidly clicks through projects, multiple loadBranches calls can run concurrently. Because there's no cancellation mechanism, whichever async call finishes last will overwrite localBranchOptions—even if it belongs to a project that's no longer selected. This means the branch dropdown could show branches for the wrong project.
For example:
- User clicks Project A →
loadBranches(A)starts - User quickly clicks Project B →
loadBranches(B)starts - B finishes first →
localBranchOptions= B branches ✓ - A finishes last →
localBranchOptions= A branches ✗ (wrong project is now displayed)
Fix by capturing the project ID at the start of the async function and skipping the state update if it no longer matches selectedModalProject?.id:
const loadBranches = async () => {
const projectId = selectedModalProject.id; // capture at start
setLocalIsLoadingBranches(true);
// ... async work ...
// After await, check if still relevant:
if (selectedModalProject.id !== projectId) return;
setLocalBranchOptions(options);
setLocalIsLoadingBranches(false);
};
- Fix race condition in branch loading when rapidly switching projects by tracking current project ID in a ref and skipping stale updates - Fix missing project navigation when creating task for non-active project by calling setSelectedProject when targetProject differs from current Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Cmd+NTest plan
Cmd+Nwith multiple projects - selector should appearCloses #1376