From 3bd8626d488a214992346dfdc9db4ad5c7eb9b09 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:27:22 +0100 Subject: [PATCH 1/4] feat: add branch/worktree support to mass edit dialog Implement worktree creation and branch assignment in the mass edit dialog to match the functionality of the add-feature and edit-feature dialogs. Changes: - Add WorkModeSelector to mass-edit-dialog.tsx with three modes: - 'Current Branch': Work on current branch (no worktree) - 'Auto Worktree': Auto-generate branch name and create worktree - 'Custom Branch': Use specified branch name and create worktree - Update handleBulkUpdate in board-view.tsx to: - Accept workMode parameter - Create worktrees for 'auto' and 'custom' modes - Auto-select created worktrees in the board header - Handle branch name generation for 'auto' mode - Add necessary props to MassEditDialog (branchSuggestions, branchCardCounts, currentBranch) Users can now bulk-assign features to a branch and automatically create/select worktrees, enabling efficient project setup with many features. Fixes #459 Co-Authored-By: Claude Sonnet 4.5 --- apps/ui/src/components/views/board-view.tsx | 107 +++++++++++++++++- .../board-view/dialogs/mass-edit-dialog.tsx | 65 ++++++++++- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 30cd4db3..58136ebc 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -492,18 +492,104 @@ export function BoardView() { // Handler for bulk updating multiple features const handleBulkUpdate = useCallback( - async (updates: Partial) => { + async (updates: Partial, workMode: 'current' | 'auto' | 'custom') => { if (!currentProject || selectedFeatureIds.size === 0) return; try { + // Determine final branch name based on work mode: + // - 'current': No branch name, work on current branch (no worktree) + // - 'auto': Auto-generate branch name based on current branch + // - 'custom': Use the provided branch name + let finalBranchName: string | undefined; + + if (workMode === 'current') { + // No worktree isolation - work directly on current branch + finalBranchName = undefined; + } else if (workMode === 'auto') { + // Auto-generate a branch name based on current branch and timestamp + const baseBranch = currentWorktreeBranch || 'main'; + const timestamp = Date.now(); + const randomSuffix = Math.random().toString(36).substring(2, 6); + finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`; + } else { + // Custom mode - use provided branch name + finalBranchName = updates.branchName || undefined; + } + + // Create worktree for 'auto' or 'custom' modes when we have a branch name + if ((workMode === 'auto' || workMode === 'custom') && finalBranchName) { + try { + const electronApi = getElectronAPI(); + if (electronApi?.worktree?.create) { + const result = await electronApi.worktree.create( + currentProject.path, + finalBranchName + ); + if (result.success && result.worktree) { + logger.info( + `Worktree for branch "${finalBranchName}" ${ + result.worktree?.isNew ? 'created' : 'already exists' + }` + ); + // Auto-select the worktree when creating/using it for bulk update + const currentWorktrees = getWorktrees(currentProject.path); + const existingWorktree = currentWorktrees.find( + (w) => w.branch === result.worktree.branch + ); + + // Only add if it doesn't already exist (to avoid duplicates) + if (!existingWorktree) { + const newWorktreeInfo = { + path: result.worktree.path, + branch: result.worktree.branch, + isMain: false, + isCurrent: false, + hasWorktree: true, + }; + setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); + } + // Select the worktree (whether it existed or was just added) + setCurrentWorktree( + currentProject.path, + result.worktree.path, + result.worktree.branch + ); + // Refresh worktree list in UI + setWorktreeRefreshKey((k) => k + 1); + } else if (!result.success) { + logger.error( + `Failed to create worktree for branch "${finalBranchName}":`, + result.error + ); + toast.error('Failed to create worktree', { + description: result.error || 'An error occurred', + }); + return; // Don't proceed with update if worktree creation failed + } + } + } catch (error) { + logger.error('Error creating worktree:', error); + toast.error('Failed to create worktree', { + description: error instanceof Error ? error.message : 'An error occurred', + }); + return; // Don't proceed with update if worktree creation failed + } + } + + // Use the final branch name in updates + const finalUpdates = { + ...updates, + branchName: finalBranchName, + }; + const api = getHttpApiClient(); const featureIds = Array.from(selectedFeatureIds); - const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates); + const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates); if (result.success) { // Update local state featureIds.forEach((featureId) => { - updateFeature(featureId, updates); + updateFeature(featureId, finalUpdates); }); toast.success(`Updated ${result.updatedCount} features`); exitSelectionMode(); @@ -517,7 +603,17 @@ export function BoardView() { toast.error('Failed to update features'); } }, - [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode] + [ + currentProject, + selectedFeatureIds, + updateFeature, + exitSelectionMode, + currentWorktreeBranch, + getWorktrees, + setWorktrees, + setCurrentWorktree, + setWorktreeRefreshKey, + ] ); // Handler for bulk deleting multiple features @@ -1325,6 +1421,9 @@ export function BoardView() { onClose={() => setShowMassEditDialog(false)} selectedFeatures={selectedFeatures} onApply={handleBulkUpdate} + branchSuggestions={branchSuggestions} + branchCardCounts={branchCardCounts} + currentBranch={currentWorktreeBranch || undefined} /> {/* Board Background Modal */} diff --git a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx index 30042a4c..2be7d32f 100644 --- a/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/mass-edit-dialog.tsx @@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label'; import { AlertCircle } from 'lucide-react'; import { modelSupportsThinking } from '@/lib/utils'; import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store'; -import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared'; +import { TestingTabContent, PrioritySelect, PlanningModeSelect, WorkModeSelector } from '../shared'; +import type { WorkMode } from '../shared'; import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector'; import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types'; import { cn } from '@/lib/utils'; @@ -23,7 +24,10 @@ interface MassEditDialogProps { open: boolean; onClose: () => void; selectedFeatures: Feature[]; - onApply: (updates: Partial) => Promise; + onApply: (updates: Partial, workMode: WorkMode) => Promise; + branchSuggestions: string[]; + branchCardCounts?: Record; + currentBranch?: string; } interface ApplyState { @@ -33,6 +37,7 @@ interface ApplyState { requirePlanApproval: boolean; priority: boolean; skipTests: boolean; + branchName: boolean; } function getMixedValues(features: Feature[]): Record { @@ -47,6 +52,7 @@ function getMixedValues(features: Feature[]): Record { ), priority: !features.every((f) => f.priority === first.priority), skipTests: !features.every((f) => f.skipTests === first.skipTests), + branchName: !features.every((f) => f.branchName === first.branchName), }; } @@ -97,7 +103,15 @@ function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: Fi ); } -export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: MassEditDialogProps) { +export function MassEditDialog({ + open, + onClose, + selectedFeatures, + onApply, + branchSuggestions, + branchCardCounts, + currentBranch, +}: MassEditDialogProps) { const [isApplying, setIsApplying] = useState(false); // Track which fields to apply @@ -108,6 +122,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas requirePlanApproval: false, priority: false, skipTests: false, + branchName: false, }); // Field values @@ -118,6 +133,18 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas const [priority, setPriority] = useState(2); const [skipTests, setSkipTests] = useState(false); + // Work mode and branch name state + const [workMode, setWorkMode] = useState(() => { + // Derive initial work mode from first selected feature's branchName + if (selectedFeatures.length > 0 && selectedFeatures[0].branchName) { + return 'custom'; + } + return 'current'; + }); + const [branchName, setBranchName] = useState(() => { + return getInitialValue(selectedFeatures, 'branchName', '') as string; + }); + // Calculate mixed values const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]); @@ -131,6 +158,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas requirePlanApproval: false, priority: false, skipTests: false, + branchName: false, }); setModel(getInitialValue(selectedFeatures, 'model', 'sonnet') as ModelAlias); setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel); @@ -138,6 +166,10 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas setRequirePlanApproval(getInitialValue(selectedFeatures, 'requirePlanApproval', false)); setPriority(getInitialValue(selectedFeatures, 'priority', 2)); setSkipTests(getInitialValue(selectedFeatures, 'skipTests', false)); + // Reset work mode and branch name + const initialBranchName = getInitialValue(selectedFeatures, 'branchName', '') as string; + setBranchName(initialBranchName); + setWorkMode(initialBranchName ? 'custom' : 'current'); } }, [open, selectedFeatures]); @@ -150,6 +182,12 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas if (applyState.requirePlanApproval) updates.requirePlanApproval = requirePlanApproval; if (applyState.priority) updates.priority = priority; if (applyState.skipTests) updates.skipTests = skipTests; + if (applyState.branchName) { + // For 'current' mode, use empty string (work on current branch) + // For 'auto' mode, use empty string (will be auto-generated) + // For 'custom' mode, use the specified branch name + updates.branchName = workMode === 'custom' ? branchName : ''; + } if (Object.keys(updates).length === 0) { onClose(); @@ -158,7 +196,7 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas setIsApplying(true); try { - await onApply(updates); + await onApply(updates, workMode); onClose(); } finally { setIsApplying(false); @@ -293,6 +331,25 @@ export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: Mas testIdPrefix="mass-edit" /> + + {/* Branch / Work Mode */} + setApplyState((prev) => ({ ...prev, branchName: apply }))} + > + + From d4076ad0cef4932d42c2cb02cdc3d20f3dd31c46 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:37:26 +0100 Subject: [PATCH 2/4] refactor: address CodeRabbit PR feedback Improvements based on CodeRabbit review comments: 1. Use getPrimaryWorktreeBranch for consistent branch detection - Replace hardcoded 'main' fallback with getPrimaryWorktreeBranch() - Ensures auto-generated branch names respect the repo's actual primary branch - Handles repos using 'master' or other primary branch names 2. Extract worktree auto-selection logic to helper function - Create addAndSelectWorktree helper to eliminate code duplication - Use helper in both onWorktreeAutoSelect and handleBulkUpdate - Reduces maintenance burden and ensures consistent behavior These changes improve code consistency and maintainability without affecting functionality. Co-Authored-By: Claude Sonnet 4.5 --- apps/ui/src/components/views/board-view.tsx | 77 +++++++++------------ 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 58136ebc..8f43b677 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -422,6 +422,31 @@ export function BoardView() { const selectedWorktreeBranch = currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main'; + // Helper function to add and select a worktree + const addAndSelectWorktree = useCallback( + (worktreeResult: { path: string; branch: string }) => { + if (!currentProject) return; + + const currentWorktrees = getWorktrees(currentProject.path); + const existingWorktree = currentWorktrees.find((w) => w.branch === worktreeResult.branch); + + // Only add if it doesn't already exist (to avoid duplicates) + if (!existingWorktree) { + const newWorktreeInfo = { + path: worktreeResult.path, + branch: worktreeResult.branch, + isMain: false, + isCurrent: false, + hasWorktree: true, + }; + setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); + } + // Select the worktree (whether it existed or was just added) + setCurrentWorktree(currentProject.path, worktreeResult.path, worktreeResult.branch); + }, + [currentProject, getWorktrees, setWorktrees, setCurrentWorktree] + ); + // Extract all action handlers into a hook const { handleAddFeature, @@ -467,26 +492,7 @@ export function BoardView() { outputFeature, projectPath: currentProject?.path || null, onWorktreeCreated: () => setWorktreeRefreshKey((k) => k + 1), - onWorktreeAutoSelect: (newWorktree) => { - if (!currentProject) return; - // Check if worktree already exists in the store (by branch name) - const currentWorktrees = getWorktrees(currentProject.path); - const existingWorktree = currentWorktrees.find((w) => w.branch === newWorktree.branch); - - // Only add if it doesn't already exist (to avoid duplicates) - if (!existingWorktree) { - const newWorktreeInfo = { - path: newWorktree.path, - branch: newWorktree.branch, - isMain: false, - isCurrent: false, - hasWorktree: true, - }; - setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); - } - // Select the worktree (whether it existed or was just added) - setCurrentWorktree(currentProject.path, newWorktree.path, newWorktree.branch); - }, + onWorktreeAutoSelect: addAndSelectWorktree, currentWorktreeBranch, }); @@ -507,7 +513,8 @@ export function BoardView() { finalBranchName = undefined; } else if (workMode === 'auto') { // Auto-generate a branch name based on current branch and timestamp - const baseBranch = currentWorktreeBranch || 'main'; + const baseBranch = + currentWorktreeBranch || getPrimaryWorktreeBranch(currentProject.path) || 'main'; const timestamp = Date.now(); const randomSuffix = Math.random().toString(36).substring(2, 6); finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`; @@ -532,28 +539,7 @@ export function BoardView() { }` ); // Auto-select the worktree when creating/using it for bulk update - const currentWorktrees = getWorktrees(currentProject.path); - const existingWorktree = currentWorktrees.find( - (w) => w.branch === result.worktree.branch - ); - - // Only add if it doesn't already exist (to avoid duplicates) - if (!existingWorktree) { - const newWorktreeInfo = { - path: result.worktree.path, - branch: result.worktree.branch, - isMain: false, - isCurrent: false, - hasWorktree: true, - }; - setWorktrees(currentProject.path, [...currentWorktrees, newWorktreeInfo]); - } - // Select the worktree (whether it existed or was just added) - setCurrentWorktree( - currentProject.path, - result.worktree.path, - result.worktree.branch - ); + addAndSelectWorktree(result.worktree); // Refresh worktree list in UI setWorktreeRefreshKey((k) => k + 1); } else if (!result.success) { @@ -609,9 +595,8 @@ export function BoardView() { updateFeature, exitSelectionMode, currentWorktreeBranch, - getWorktrees, - setWorktrees, - setCurrentWorktree, + getPrimaryWorktreeBranch, + addAndSelectWorktree, setWorktreeRefreshKey, ] ); From cc4f39a6abb80dc660281dda3d53dd6bf94fc7f0 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:38:09 +0100 Subject: [PATCH 3/4] chore: fix formatting issues for CI Fix Prettier formatting in two files: - apps/server/src/lib/sdk-options.ts: Split long arrays to one item per line - docs/docker-isolation.md: Align markdown table columns Resolves CI format check failures. Co-Authored-By: Claude Sonnet 4.5 --- apps/server/src/lib/sdk-options.ts | 24 ++++++++++++++++++++++-- docs/docker-isolation.md | 15 +++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index ff3d6067..cc1df2f5 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -129,10 +129,30 @@ export const TOOL_PRESETS = { specGeneration: ['Read', 'Glob', 'Grep'] as const, /** Full tool access for feature implementation */ - fullAccess: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + fullAccess: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, /** Tools for chat/interactive mode */ - chat: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + chat: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, } as const; /** diff --git a/docs/docker-isolation.md b/docs/docker-isolation.md index ad7c712a..379e8c0d 100644 --- a/docs/docker-isolation.md +++ b/docs/docker-isolation.md @@ -136,12 +136,11 @@ volumes: ## Troubleshooting -| Problem | Solution | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | -| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | -| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | -| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. | -| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. | +| Problem | Solution | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | +| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | +| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | +| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. | +| OpenCode not detected | Mount `~/.local/share/opencode` to `/home/automaker/.local/share/opencode`. Make sure you've run `opencode auth login` on your host first. | | File permission errors | Rebuild with `UID=$(id -u) GID=$(id -g) docker-compose build` to match container user to your host user. See [Fixing File Permission Issues](#fixing-file-permission-issues). | - From 7ef525effac6b6058b3555103dde9dfe09ec71f9 Mon Sep 17 00:00:00 2001 From: Shirone Date: Tue, 13 Jan 2026 18:51:20 +0100 Subject: [PATCH 4/4] fix: clarify comments on branch name handling in BoardView Updated comments in BoardView to better explain the behavior of the 'current' work mode. The changes specify that an empty string clears the branch assignment, allowing work to proceed on the main/current branch. This enhances code readability and understanding of branch management logic. --- apps/ui/src/components/views/board-view.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 8f43b677..f51357d0 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -503,14 +503,14 @@ export function BoardView() { try { // Determine final branch name based on work mode: - // - 'current': No branch name, work on current branch (no worktree) + // - 'current': Empty string to clear branch assignment (work on main/current branch) // - 'auto': Auto-generate branch name based on current branch // - 'custom': Use the provided branch name let finalBranchName: string | undefined; if (workMode === 'current') { - // No worktree isolation - work directly on current branch - finalBranchName = undefined; + // Empty string clears the branch assignment, moving features to main/current branch + finalBranchName = ''; } else if (workMode === 'auto') { // Auto-generate a branch name based on current branch and timestamp const baseBranch =