Merge branch 'main' into refactor/frontend

This commit is contained in:
Cody Seibert
2025-12-19 14:42:31 -05:00
24 changed files with 1205 additions and 879 deletions

View File

@@ -12,7 +12,7 @@ import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Resolve workspace root (apps/app/scripts -> workspace root)
// Resolve workspace root (apps/ui/scripts -> workspace root)
const WORKSPACE_ROOT = path.resolve(__dirname, "../../..");
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");

View File

@@ -1,11 +1,6 @@
import { useState, useEffect } from "react";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import { Input } from "@/components/ui/input";
@@ -115,8 +110,10 @@ export function SessionManager({
new Set()
);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] =
useState<SessionListItem | null>(null);
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] =
useState(false);
// Check running state for all sessions
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
@@ -233,11 +230,7 @@ export function SessionManager({
const api = getElectronAPI();
if (!editingName.trim() || !api?.sessions) return;
const result = await api.sessions.update(
sessionId,
editingName,
undefined
);
const result = await api.sessions.update(sessionId, editingName, undefined);
if (result.success) {
setEditingSessionId(null);

View File

@@ -7,6 +7,7 @@ interface BranchAutocompleteProps {
value: string;
onChange: (value: string) => void;
branches: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
placeholder?: string;
className?: string;
disabled?: boolean;
@@ -18,6 +19,7 @@ export function BranchAutocomplete({
value,
onChange,
branches,
branchCardCounts,
placeholder = "Select a branch...",
className,
disabled = false,
@@ -27,12 +29,22 @@ export function BranchAutocomplete({
// Always include "main" at the top of suggestions
const branchOptions: AutocompleteOption[] = React.useMemo(() => {
const branchSet = new Set(["main", ...branches]);
return Array.from(branchSet).map((branch) => ({
value: branch,
label: branch,
badge: branch === "main" ? "default" : undefined,
}));
}, [branches]);
return Array.from(branchSet).map((branch) => {
const cardCount = branchCardCounts?.[branch];
// Show card count if available, otherwise show "default" for main branch only
const badge = branchCardCounts !== undefined
? String(cardCount ?? 0)
: branch === "main"
? "default"
: undefined;
return {
value: branch,
label: branch,
badge,
};
});
}, [branches, branchCardCounts]);
return (
<Autocomplete

View File

@@ -269,6 +269,17 @@ export function BoardView() {
fetchBranches();
}, [currentProject, worktreeRefreshKey]);
// Calculate unarchived card counts per branch
const branchCardCounts = useMemo(() => {
return hookFeatures.reduce((counts, feature) => {
if (feature.status !== "completed") {
const branch = feature.branchName ?? "main";
counts[branch] = (counts[branch] || 0) + 1;
}
return counts;
}, {} as Record<string, number>);
}, [hookFeatures]);
// Custom collision detection that prioritizes columns over cards
const collisionDetectionStrategy = useCallback((args: any) => {
// First, check if pointer is within a column
@@ -301,14 +312,14 @@ export function BoardView() {
});
if (matchesRemovedWorktree) {
// Reset the feature's branch assignment
persistFeatureUpdate(feature.id, {
branchName: null as unknown as string | undefined,
});
// Reset the feature's branch assignment - update both local state and persist
const updates = { branchName: null as unknown as string | undefined };
updateFeature(feature.id, updates);
persistFeatureUpdate(feature.id, updates);
}
});
},
[hookFeatures, persistFeatureUpdate]
[hookFeatures, updateFeature, persistFeatureUpdate]
);
// Get in-progress features for keyboard shortcuts (needed before actions hook)
@@ -417,6 +428,18 @@ export function BoardView() {
hookFeaturesRef.current = hookFeatures;
}, [hookFeatures]);
// Use a ref to track running tasks to avoid effect re-runs that clear pendingFeaturesRef
const runningAutoTasksRef = useRef(runningAutoTasks);
useEffect(() => {
runningAutoTasksRef.current = runningAutoTasks;
}, [runningAutoTasks]);
// Keep latest start handler without retriggering the auto mode effect
const handleStartImplementationRef = useRef(handleStartImplementation);
useEffect(() => {
handleStartImplementationRef.current = handleStartImplementation;
}, [handleStartImplementation]);
// Track features that are pending (started but not yet confirmed running)
const pendingFeaturesRef = useRef<Set<string>>(new Set());
@@ -484,8 +507,9 @@ export function BoardView() {
}
// Count currently running tasks + pending features
// Use ref to get the latest running tasks without causing effect re-runs
const currentRunning =
runningAutoTasks.length + pendingFeaturesRef.current.size;
runningAutoTasksRef.current.length + pendingFeaturesRef.current.size;
const availableSlots = maxConcurrency - currentRunning;
// No available slots, skip check
@@ -540,6 +564,10 @@ export function BoardView() {
// Start features up to available slots
const featuresToStart = eligibleFeatures.slice(0, availableSlots);
const startImplementation = handleStartImplementationRef.current;
if (!startImplementation) {
return;
}
for (const feature of featuresToStart) {
// Check again before starting each feature
@@ -565,7 +593,7 @@ export function BoardView() {
}
// Start the implementation - server will derive workDir from feature.branchName
const started = await handleStartImplementation(feature);
const started = await startImplementation(feature);
// If successfully started, track it as pending until we receive the start event
if (started) {
@@ -579,7 +607,7 @@ export function BoardView() {
// Check immediately, then every 3 seconds
checkAndStartFeatures();
const interval = setInterval(checkAndStartFeatures, 3000);
const interval = setInterval(checkAndStartFeatures, 1000);
return () => {
// Mark as inactive to prevent any pending async operations from continuing
@@ -591,7 +619,8 @@ export function BoardView() {
}, [
autoMode.isRunning,
currentProject,
runningAutoTasks,
// runningAutoTasks is accessed via runningAutoTasksRef to prevent effect re-runs
// that would clear pendingFeaturesRef and cause concurrency issues
maxConcurrency,
// hookFeatures is accessed via hookFeaturesRef to prevent effect re-runs
currentWorktreeBranch,
@@ -600,7 +629,6 @@ export function BoardView() {
isPrimaryWorktreeBranch,
enableDependencyBlocking,
persistFeatureUpdate,
handleStartImplementation,
]);
// Use keyboard shortcuts hook (after actions hook)
@@ -639,7 +667,9 @@ export function BoardView() {
// Find feature for pending plan approval
const pendingApprovalFeature = useMemo(() => {
if (!pendingPlanApproval) return null;
return hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null;
return (
hookFeatures.find((f) => f.id === pendingPlanApproval.featureId) || null
);
}, [pendingPlanApproval, hookFeatures]);
// Handle plan approval
@@ -665,10 +695,10 @@ export function BoardView() {
if (result.success) {
// Immediately update local feature state to hide "Approve Plan" button
// Get current feature to preserve version
const currentFeature = hookFeatures.find(f => f.id === featureId);
const currentFeature = hookFeatures.find((f) => f.id === featureId);
updateFeature(featureId, {
planSpec: {
status: 'approved',
status: "approved",
content: editedPlan || pendingPlanApproval.planContent,
version: currentFeature?.planSpec?.version || 1,
approvedAt: new Date().toISOString(),
@@ -687,7 +717,14 @@ export function BoardView() {
setPendingPlanApproval(null);
}
},
[pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures]
[
pendingPlanApproval,
currentProject,
setPendingPlanApproval,
updateFeature,
loadFeatures,
hookFeatures,
]
);
// Handle plan rejection
@@ -714,11 +751,11 @@ export function BoardView() {
if (result.success) {
// Immediately update local feature state
// Get current feature to preserve version
const currentFeature = hookFeatures.find(f => f.id === featureId);
const currentFeature = hookFeatures.find((f) => f.id === featureId);
updateFeature(featureId, {
status: 'backlog',
status: "backlog",
planSpec: {
status: 'rejected',
status: "rejected",
content: pendingPlanApproval.planContent,
version: currentFeature?.planSpec?.version || 1,
reviewedByUser: true,
@@ -736,7 +773,14 @@ export function BoardView() {
setPendingPlanApproval(null);
}
},
[pendingPlanApproval, currentProject, setPendingPlanApproval, updateFeature, loadFeatures, hookFeatures]
[
pendingPlanApproval,
currentProject,
setPendingPlanApproval,
updateFeature,
loadFeatures,
hookFeatures,
]
);
// Handle opening approval dialog from feature card button
@@ -747,7 +791,7 @@ export function BoardView() {
// Determine the planning mode for approval (skip should never have a plan requiring approval)
const mode = feature.planningMode;
const approvalMode: "lite" | "spec" | "full" =
mode === 'lite' || mode === 'spec' || mode === 'full' ? mode : 'spec';
mode === "lite" || mode === "spec" || mode === "full" ? mode : "spec";
// Re-open the approval dialog with the feature's plan data
setPendingPlanApproval({
@@ -832,6 +876,7 @@ export function BoardView() {
}}
onRemovedWorktrees={handleRemovedWorktrees}
runningFeatureIds={runningAutoTasks}
branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({
id: f.id,
branchName: f.branchName,
@@ -928,6 +973,7 @@ export function BoardView() {
onAdd={handleAddFeature}
categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
defaultSkipTests={defaultSkipTests}
defaultBranch={selectedWorktreeBranch}
currentBranch={currentWorktreeBranch || undefined}
@@ -943,6 +989,7 @@ export function BoardView() {
onUpdate={handleUpdateFeature}
categorySuggestions={categorySuggestions}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentWorktreeBranch || undefined}
isMaximized={isMaximized}
showProfilesOnly={showProfilesOnly}
@@ -1064,15 +1111,24 @@ export function BoardView() {
onOpenChange={setShowDeleteWorktreeDialog}
projectPath={currentProject.path}
worktree={selectedWorktreeForAction}
affectedFeatureCount={
selectedWorktreeForAction
? hookFeatures.filter(
(f) => f.branchName === selectedWorktreeForAction.branch
).length
: 0
}
onDeleted={(deletedWorktree, _deletedBranch) => {
// Reset features that were assigned to the deleted worktree (by branch)
hookFeatures.forEach((feature) => {
// Match by branch name since worktreePath is no longer stored
if (feature.branchName === deletedWorktree.branch) {
// Reset the feature's branch assignment
persistFeatureUpdate(feature.id, {
// Reset the feature's branch assignment - update both local state and persist
const updates = {
branchName: null as unknown as string | undefined,
});
};
updateFeature(feature.id, updates);
persistFeatureUpdate(feature.id, updates);
}
});

View File

@@ -73,6 +73,7 @@ interface AddFeatureDialogProps {
}) => void;
categorySuggestions: string[];
branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
defaultSkipTests: boolean;
defaultBranch?: string;
currentBranch?: string;
@@ -87,6 +88,7 @@ export function AddFeatureDialog({
onAdd,
categorySuggestions,
branchSuggestions,
branchCardCounts,
defaultSkipTests,
defaultBranch = "main",
currentBranch,
@@ -116,11 +118,16 @@ export function AddFeatureDialog({
const [enhancementMode, setEnhancementMode] = useState<
"improve" | "technical" | "simplify" | "acceptance"
>("improve");
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
const [planningMode, setPlanningMode] = useState<PlanningMode>("skip");
const [requirePlanApproval, setRequirePlanApproval] = useState(false);
// Get enhancement model, planning mode defaults, and worktrees setting from store
const { enhancementModel, defaultPlanningMode, defaultRequirePlanApproval, useWorktrees } = useAppStore();
const {
enhancementModel,
defaultPlanningMode,
defaultRequirePlanApproval,
useWorktrees,
} = useAppStore();
// Sync defaults when dialog opens
useEffect(() => {
@@ -134,7 +141,13 @@ export function AddFeatureDialog({
setPlanningMode(defaultPlanningMode);
setRequirePlanApproval(defaultRequirePlanApproval);
}
}, [open, defaultSkipTests, defaultBranch, defaultPlanningMode, defaultRequirePlanApproval]);
}, [
open,
defaultSkipTests,
defaultBranch,
defaultPlanningMode,
defaultRequirePlanApproval,
]);
const handleAdd = () => {
if (!newFeature.description.trim()) {
@@ -158,7 +171,7 @@ export function AddFeatureDialog({
// If currentBranch is provided (non-primary worktree), use it
// Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary)
const finalBranchName = useCurrentBranch
? (currentBranch || "")
? currentBranch || ""
: newFeature.branchName || "";
onAdd({
@@ -399,6 +412,7 @@ export function AddFeatureDialog({
setNewFeature({ ...newFeature, branchName: value })
}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch}
testIdPrefix="feature"
/>
@@ -481,7 +495,10 @@ export function AddFeatureDialog({
</TabsContent>
{/* Options Tab */}
<TabsContent value="options" className="space-y-4 overflow-y-auto cursor-default">
<TabsContent
value="options"
className="space-y-4 overflow-y-auto cursor-default"
>
{/* Planning Mode Section */}
<PlanningModeSelector
mode={planningMode}
@@ -516,9 +533,7 @@ export function AddFeatureDialog({
hotkeyActive={open}
data-testid="confirm-add-feature"
disabled={
useWorktrees &&
!useCurrentBranch &&
!newFeature.branchName.trim()
useWorktrees && !useCurrentBranch && !newFeature.branchName.trim()
}
>
Add Feature

View File

@@ -11,7 +11,7 @@ import {
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Loader2, Trash2, AlertTriangle } from "lucide-react";
import { Loader2, Trash2, AlertTriangle, FileWarning } from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { toast } from "sonner";
@@ -29,6 +29,8 @@ interface DeleteWorktreeDialogProps {
projectPath: string;
worktree: WorktreeInfo | null;
onDeleted: (deletedWorktree: WorktreeInfo, deletedBranch: boolean) => void;
/** Number of features assigned to this worktree's branch */
affectedFeatureCount?: number;
}
export function DeleteWorktreeDialog({
@@ -37,6 +39,7 @@ export function DeleteWorktreeDialog({
projectPath,
worktree,
onDeleted,
affectedFeatureCount = 0,
}: DeleteWorktreeDialogProps) {
const [deleteBranch, setDeleteBranch] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -99,6 +102,18 @@ export function DeleteWorktreeDialog({
?
</span>
{affectedFeatureCount > 0 && (
<div className="flex items-start gap-2 p-3 rounded-md bg-orange-500/10 border border-orange-500/20 mt-2">
<FileWarning className="w-4 h-4 text-orange-500 mt-0.5 flex-shrink-0" />
<span className="text-orange-500 text-sm">
{affectedFeatureCount} feature{affectedFeatureCount !== 1 ? "s" : ""}{" "}
{affectedFeatureCount !== 1 ? "are" : "is"} assigned to this
branch. {affectedFeatureCount !== 1 ? "They" : "It"} will be
unassigned and moved to the main worktree.
</span>
</div>
)}
{worktree.hasChanges && (
<div className="flex items-start gap-2 p-3 rounded-md bg-yellow-500/10 border border-yellow-500/20 mt-2">
<AlertTriangle className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />

View File

@@ -76,6 +76,7 @@ interface EditFeatureDialogProps {
) => void;
categorySuggestions: string[];
branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
currentBranch?: string;
isMaximized: boolean;
showProfilesOnly: boolean;
@@ -89,6 +90,7 @@ export function EditFeatureDialog({
onUpdate,
categorySuggestions,
branchSuggestions,
branchCardCounts,
currentBranch,
isMaximized,
showProfilesOnly,
@@ -388,6 +390,7 @@ export function EditFeatureDialog({
})
}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch}
disabled={editingFeature.status !== "backlog"}
testIdPrefix="edit-feature"

View File

@@ -82,8 +82,8 @@ export function useBoardActions({
} = useAppStore();
const autoMode = useAutoMode();
// Note: getOrCreateWorktreeForFeature removed - worktrees are now created server-side
// at execution time based on feature.branchName
// Worktrees are created when adding/editing features with a branch name
// This ensures the worktree exists before the feature starts execution
const handleAddFeature = useCallback(
async (featureData: {
@@ -100,24 +100,58 @@ export function useBoardActions({
planningMode: PlanningMode;
requirePlanApproval: boolean;
}) => {
// Simplified: Only store branchName, no worktree creation on add
// Worktrees are created at execution time (when feature starts)
// Empty string means "unassigned" (show only on primary worktree) - convert to undefined
// Non-empty string is the actual branch name (for non-primary worktrees)
const finalBranchName = featureData.branchName || undefined;
// If worktrees enabled and a branch is specified, create the worktree now
// This ensures the worktree exists before the feature starts
if (useWorktrees && finalBranchName && currentProject) {
try {
const api = getElectronAPI();
if (api?.worktree?.create) {
const result = await api.worktree.create(
currentProject.path,
finalBranchName
);
if (result.success) {
console.log(
`[Board] Worktree for branch "${finalBranchName}" ${
result.worktree?.isNew ? "created" : "already exists"
}`
);
// Refresh worktree list in UI
onWorktreeCreated?.();
} else {
console.error(
`[Board] Failed to create worktree for branch "${finalBranchName}":`,
result.error
);
toast.error("Failed to create worktree", {
description: result.error || "An error occurred",
});
}
}
} catch (error) {
console.error("[Board] Error creating worktree:", error);
toast.error("Failed to create worktree", {
description:
error instanceof Error ? error.message : "An error occurred",
});
}
}
const newFeatureData = {
...featureData,
status: "backlog" as const,
branchName: finalBranchName,
// No worktreePath - derived at runtime from branchName
};
const createdFeature = addFeature(newFeatureData);
// Must await to ensure feature exists on server before user can drag it
await persistFeatureCreate(createdFeature);
saveCategory(featureData.category);
},
[addFeature, persistFeatureCreate, saveCategory]
[addFeature, persistFeatureCreate, saveCategory, useWorktrees, currentProject, onWorktreeCreated]
);
const handleUpdateFeature = useCallback(
@@ -139,6 +173,43 @@ export function useBoardActions({
) => {
const finalBranchName = updates.branchName || undefined;
// If worktrees enabled and a branch is specified, create the worktree now
// This ensures the worktree exists before the feature starts
if (useWorktrees && finalBranchName && currentProject) {
try {
const api = getElectronAPI();
if (api?.worktree?.create) {
const result = await api.worktree.create(
currentProject.path,
finalBranchName
);
if (result.success) {
console.log(
`[Board] Worktree for branch "${finalBranchName}" ${
result.worktree?.isNew ? "created" : "already exists"
}`
);
// Refresh worktree list in UI
onWorktreeCreated?.();
} else {
console.error(
`[Board] Failed to create worktree for branch "${finalBranchName}":`,
result.error
);
toast.error("Failed to create worktree", {
description: result.error || "An error occurred",
});
}
}
} catch (error) {
console.error("[Board] Error creating worktree:", error);
toast.error("Failed to create worktree", {
description:
error instanceof Error ? error.message : "An error occurred",
});
}
}
const finalUpdates = {
...updates,
branchName: finalBranchName,
@@ -151,7 +222,7 @@ export function useBoardActions({
}
setEditingFeature(null);
},
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature]
[updateFeature, persistFeatureUpdate, saveCategory, setEditingFeature, useWorktrees, currentProject, onWorktreeCreated]
);
const handleDeleteFeature = useCallback(

View File

@@ -10,6 +10,7 @@ interface BranchSelectorProps {
branchName: string;
onBranchNameChange: (branchName: string) => void;
branchSuggestions: string[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
currentBranch?: string;
disabled?: boolean;
testIdPrefix?: string;
@@ -21,6 +22,7 @@ export function BranchSelector({
branchName,
onBranchNameChange,
branchSuggestions,
branchCardCounts,
currentBranch,
disabled = false,
testIdPrefix = "branch",
@@ -69,6 +71,7 @@ export function BranchSelector({
value={branchName}
onChange={onBranchNameChange}
branches={branchSuggestions}
branchCardCounts={branchCardCounts}
placeholder="Select or create branch..."
data-testid={`${testIdPrefix}-input`}
disabled={disabled}

View File

@@ -8,6 +8,7 @@ import { WorktreeActionsDropdown } from "./worktree-actions-dropdown";
interface WorktreeTabProps {
worktree: WorktreeInfo;
cardCount?: number; // Number of unarchived cards for this branch
isSelected: boolean;
isRunning: boolean;
isActivating: boolean;
@@ -43,6 +44,7 @@ interface WorktreeTabProps {
export function WorktreeTab({
worktree,
cardCount,
isSelected,
isRunning,
isActivating,
@@ -96,9 +98,9 @@ export function WorktreeTab({
<RefreshCw className="w-3 h-3 animate-spin" />
)}
{worktree.branch}
{worktree.hasChanges && (
{cardCount !== undefined && cardCount > 0 && (
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
{worktree.changedFilesCount}
{cardCount}
</span>
)}
</Button>
@@ -139,9 +141,9 @@ export function WorktreeTab({
<RefreshCw className="w-3 h-3 animate-spin" />
)}
{worktree.branch}
{worktree.hasChanges && (
{cardCount !== undefined && cardCount > 0 && (
<span className="inline-flex items-center justify-center h-4 min-w-[1rem] px-1 text-[10px] font-medium rounded bg-background/80 text-foreground border border-border">
{worktree.changedFilesCount}
{cardCount}
</span>
)}
</Button>

View File

@@ -35,5 +35,6 @@ export interface WorktreePanelProps {
onRemovedWorktrees?: (removedWorktrees: Array<{ path: string; branch: string }>) => void;
runningFeatureIds?: string[];
features?: FeatureInfo[];
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
refreshTrigger?: number;
}

View File

@@ -23,6 +23,7 @@ export function WorktreePanel({
onRemovedWorktrees,
runningFeatureIds = [],
features = [],
branchCardCounts,
refreshTrigger = 0,
}: WorktreePanelProps) {
const {
@@ -109,43 +110,47 @@ export function WorktreePanel({
<span className="text-sm text-muted-foreground mr-2">Branch:</span>
<div className="flex items-center gap-1 flex-wrap">
{worktrees.map((worktree) => (
<WorktreeTab
key={worktree.path}
worktree={worktree}
isSelected={isWorktreeSelected(worktree)}
isRunning={hasRunningFeatures(worktree)}
isActivating={isActivating}
isDevServerRunning={isDevServerRunning(worktree)}
devServerInfo={getDevServerInfo(worktree)}
defaultEditorName={defaultEditorName}
branches={branches}
filteredBranches={filteredBranches}
branchFilter={branchFilter}
isLoadingBranches={isLoadingBranches}
isSwitching={isSwitching}
isPulling={isPulling}
isPushing={isPushing}
isStartingDevServer={isStartingDevServer}
aheadCount={aheadCount}
behindCount={behindCount}
onSelectWorktree={handleSelectWorktree}
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)}
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)}
onBranchFilterChange={setBranchFilter}
onSwitchBranch={handleSwitchBranch}
onCreateBranch={onCreateBranch}
onPull={handlePull}
onPush={handlePush}
onOpenInEditor={handleOpenInEditor}
onCommit={onCommit}
onCreatePR={onCreatePR}
onDeleteWorktree={onDeleteWorktree}
onStartDevServer={handleStartDevServer}
onStopDevServer={handleStopDevServer}
onOpenDevServerUrl={handleOpenDevServerUrl}
/>
))}
{worktrees.map((worktree) => {
const cardCount = branchCardCounts?.[worktree.branch];
return (
<WorktreeTab
key={worktree.path}
worktree={worktree}
cardCount={cardCount}
isSelected={isWorktreeSelected(worktree)}
isRunning={hasRunningFeatures(worktree)}
isActivating={isActivating}
isDevServerRunning={isDevServerRunning(worktree)}
devServerInfo={getDevServerInfo(worktree)}
defaultEditorName={defaultEditorName}
branches={branches}
filteredBranches={filteredBranches}
branchFilter={branchFilter}
isLoadingBranches={isLoadingBranches}
isSwitching={isSwitching}
isPulling={isPulling}
isPushing={isPushing}
isStartingDevServer={isStartingDevServer}
aheadCount={aheadCount}
behindCount={behindCount}
onSelectWorktree={handleSelectWorktree}
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)}
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)}
onBranchFilterChange={setBranchFilter}
onSwitchBranch={handleSwitchBranch}
onCreateBranch={onCreateBranch}
onPull={handlePull}
onPush={handlePush}
onOpenInEditor={handleOpenInEditor}
onCommit={onCommit}
onCreatePR={onCreatePR}
onDeleteWorktree={onDeleteWorktree}
onStartDevServer={handleStartDevServer}
onStopDevServer={handleStopDevServer}
onOpenDevServerUrl={handleOpenDevServerUrl}
/>
);
})}
<Button
variant="ghost"

View File

@@ -1,4 +1,3 @@
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
@@ -53,7 +52,9 @@ function CollapsibleSection({
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-brand-500/10 text-brand-500">
<Icon className="w-4 h-4" />
</div>
<span className="flex-1 font-medium text-foreground">{section.title}</span>
<span className="flex-1 font-medium text-foreground">
{section.title}
</span>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
@@ -86,19 +87,30 @@ function CodeBlock({ children, title }: { children: string; title?: string }) {
);
}
function FeatureList({ items }: { items: { icon: React.ElementType; title: string; description: string }[] }) {
function FeatureList({
items,
}: {
items: { icon: React.ElementType; title: string; description: string }[];
}) {
return (
<div className="grid gap-3 mt-3">
{items.map((item, index) => {
const ItemIcon = item.icon;
return (
<div key={index} className="flex items-start gap-3 p-3 rounded-lg bg-muted/30 border border-border/50">
<div
key={index}
className="flex items-start gap-3 p-3 rounded-lg bg-muted/30 border border-border/50"
>
<div className="flex items-center justify-center w-6 h-6 rounded bg-brand-500/10 text-brand-500 shrink-0 mt-0.5">
<ItemIcon className="w-3.5 h-3.5" />
</div>
<div>
<div className="font-medium text-foreground text-sm">{item.title}</div>
<div className="text-xs text-muted-foreground mt-0.5">{item.description}</div>
<div className="font-medium text-foreground text-sm">
{item.title}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{item.description}
</div>
</div>
</div>
);
@@ -108,7 +120,9 @@ function FeatureList({ items }: { items: { icon: React.ElementType; title: strin
}
export function WikiView() {
const [openSections, setOpenSections] = useState<Set<string>>(new Set(["overview"]));
const [openSections, setOpenSections] = useState<Set<string>>(
new Set(["overview"])
);
const toggleSection = (id: string) => {
setOpenSections((prev) => {
@@ -138,14 +152,21 @@ export function WikiView() {
content: (
<div className="space-y-3">
<p>
<strong className="text-foreground">Automaker</strong> is an autonomous AI development studio that helps developers build software faster using AI agents.
<strong className="text-foreground">Automaker</strong> is an
autonomous AI development studio that helps developers build
software faster using AI agents.
</p>
<p>
At its core, Automaker provides a visual Kanban board to manage features. When you're ready, AI agents automatically implement those features in your codebase, complete with git worktree isolation for safe parallel development.
At its core, Automaker provides a visual Kanban board to manage
features. When you're ready, AI agents automatically implement those
features in your codebase, complete with git worktree isolation for
safe parallel development.
</p>
<div className="p-3 rounded-lg bg-brand-500/10 border border-brand-500/20 mt-4">
<p className="text-brand-400 text-sm">
Think of it as having a team of AI developers that can work on multiple features simultaneously while you focus on the bigger picture.
Think of it as having a team of AI developers that can work on
multiple features simultaneously while you focus on the bigger
picture.
</p>
</div>
</div>
@@ -160,17 +181,21 @@ export function WikiView() {
<p>Automaker is built as a monorepo with two main applications:</p>
<ul className="list-disc list-inside space-y-2 ml-2">
<li>
<strong className="text-foreground">apps/app</strong> - Next.js + Electron frontend for the desktop application
<strong className="text-foreground">apps/ui</strong> - Next.js +
Electron frontend for the desktop application
</li>
<li>
<strong className="text-foreground">apps/server</strong> - Express backend handling API requests and agent orchestration
<strong className="text-foreground">apps/server</strong> - Express
backend handling API requests and agent orchestration
</li>
</ul>
<div className="mt-4 space-y-2">
<p className="font-medium text-foreground">Key Technologies:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li>Electron wraps Next.js for cross-platform desktop support</li>
<li>Real-time communication via WebSocket for live agent updates</li>
<li>
Real-time communication via WebSocket for live agent updates
</li>
<li>State management with Zustand for reactive UI updates</li>
<li>Claude Agent SDK for AI capabilities</li>
</ul>
@@ -189,42 +214,50 @@ export function WikiView() {
{
icon: LayoutGrid,
title: "Kanban Board",
description: "4 columns: Backlog, In Progress, Waiting Approval, Verified. Drag and drop to manage feature lifecycle.",
description:
"4 columns: Backlog, In Progress, Waiting Approval, Verified. Drag and drop to manage feature lifecycle.",
},
{
icon: Bot,
title: "AI Agent Integration",
description: "Powered by Claude via the Agent SDK with full file, bash, and git access.",
description:
"Powered by Claude via the Agent SDK with full file, bash, and git access.",
},
{
icon: Cpu,
title: "Multi-Model Support",
description: "Claude Haiku/Sonnet/Opus models. Choose the right model for each task.",
description:
"Claude Haiku/Sonnet/Opus models. Choose the right model for each task.",
},
{
icon: Brain,
title: "Extended Thinking",
description: "Configurable thinking levels (none, low, medium, high, ultrathink) for complex tasks.",
description:
"Configurable thinking levels (none, low, medium, high, ultrathink) for complex tasks.",
},
{
icon: Zap,
title: "Real-time Streaming",
description: "Watch AI agents work in real-time with live output streaming.",
description:
"Watch AI agents work in real-time with live output streaming.",
},
{
icon: GitBranch,
title: "Git Worktree Isolation",
description: "Each feature runs in its own git worktree for safe parallel development.",
description:
"Each feature runs in its own git worktree for safe parallel development.",
},
{
icon: Users,
title: "AI Profiles",
description: "Pre-configured model + thinking level combinations for different task types.",
description:
"Pre-configured model + thinking level combinations for different task types.",
},
{
icon: Terminal,
title: "Integrated Terminal",
description: "Built-in terminal with tab support and split panes.",
description:
"Built-in terminal with tab support and split panes.",
},
{
icon: Keyboard,
@@ -234,7 +267,8 @@ export function WikiView() {
{
icon: Palette,
title: "14 Themes",
description: "From light to dark, retro to synthwave - pick your style.",
description:
"From light to dark, retro to synthwave - pick your style.",
},
{
icon: Image,
@@ -244,7 +278,8 @@ export function WikiView() {
{
icon: TestTube,
title: "Test Integration",
description: "Automatic test running and TDD support for quality assurance.",
description:
"Automatic test running and TDD support for quality assurance.",
},
]}
/>
@@ -257,39 +292,63 @@ export function WikiView() {
icon: GitBranch,
content: (
<div className="space-y-3">
<p>Here's what happens when you use Automaker to implement a feature:</p>
<p>
Here's what happens when you use Automaker to implement a feature:
</p>
<ol className="list-decimal list-inside space-y-3 ml-2 mt-4">
<li className="text-foreground">
<strong>Create Feature</strong>
<p className="text-muted-foreground ml-5 mt-1">Add a new feature card to the Kanban board with description and steps</p>
<p className="text-muted-foreground ml-5 mt-1">
Add a new feature card to the Kanban board with description and
steps
</p>
</li>
<li className="text-foreground">
<strong>Feature Saved</strong>
<p className="text-muted-foreground ml-5 mt-1">Feature saved to <code className="px-1 py-0.5 bg-muted rounded text-xs">.automaker/features/&#123;id&#125;/feature.json</code></p>
<p className="text-muted-foreground ml-5 mt-1">
Feature saved to{" "}
<code className="px-1 py-0.5 bg-muted rounded text-xs">
.automaker/features/&#123;id&#125;/feature.json
</code>
</p>
</li>
<li className="text-foreground">
<strong>Start Work</strong>
<p className="text-muted-foreground ml-5 mt-1">Drag to "In Progress" or enable auto mode to start implementation</p>
<p className="text-muted-foreground ml-5 mt-1">
Drag to "In Progress" or enable auto mode to start
implementation
</p>
</li>
<li className="text-foreground">
<strong>Git Worktree Created</strong>
<p className="text-muted-foreground ml-5 mt-1">Backend AutoModeService creates isolated git worktree (if enabled)</p>
<p className="text-muted-foreground ml-5 mt-1">
Backend AutoModeService creates isolated git worktree (if
enabled)
</p>
</li>
<li className="text-foreground">
<strong>Agent Executes</strong>
<p className="text-muted-foreground ml-5 mt-1">Claude Agent SDK runs with file/bash/git tool access</p>
<p className="text-muted-foreground ml-5 mt-1">
Claude Agent SDK runs with file/bash/git tool access
</p>
</li>
<li className="text-foreground">
<strong>Progress Streamed</strong>
<p className="text-muted-foreground ml-5 mt-1">Real-time updates via WebSocket as agent works</p>
<p className="text-muted-foreground ml-5 mt-1">
Real-time updates via WebSocket as agent works
</p>
</li>
<li className="text-foreground">
<strong>Completion</strong>
<p className="text-muted-foreground ml-5 mt-1">On success, feature moves to "waiting_approval" for your review</p>
<p className="text-muted-foreground ml-5 mt-1">
On success, feature moves to "waiting_approval" for your review
</p>
</li>
<li className="text-foreground">
<strong>Verify</strong>
<p className="text-muted-foreground ml-5 mt-1">Review changes and move to "verified" when satisfied</p>
<p className="text-muted-foreground ml-5 mt-1">
Review changes and move to "verified" when satisfied
</p>
</li>
</ol>
</div>
@@ -301,9 +360,11 @@ export function WikiView() {
icon: FolderTree,
content: (
<div>
<p className="mb-3">The Automaker codebase is organized as follows:</p>
<p className="mb-3">
The Automaker codebase is organized as follows:
</p>
<CodeBlock title="Directory Structure">
{`/automaker/
{`/automaker/
├── apps/
│ ├── app/ # Frontend (Next.js + Electron)
│ │ ├── electron/ # Electron main process
@@ -332,18 +393,46 @@ export function WikiView() {
<p>The main UI components that make up Automaker:</p>
<div className="grid gap-2 mt-4">
{[
{ file: "sidebar.tsx", desc: "Main navigation with project picker and view switching" },
{ file: "board-view.tsx", desc: "Kanban board with drag-and-drop cards" },
{ file: "agent-view.tsx", desc: "AI chat interface for conversational development" },
{
file: "sidebar.tsx",
desc: "Main navigation with project picker and view switching",
},
{
file: "board-view.tsx",
desc: "Kanban board with drag-and-drop cards",
},
{
file: "agent-view.tsx",
desc: "AI chat interface for conversational development",
},
{ file: "spec-view.tsx", desc: "Project specification editor" },
{ file: "context-view.tsx", desc: "Context file manager for AI context" },
{ file: "terminal-view.tsx", desc: "Integrated terminal with splits and tabs" },
{ file: "profiles-view.tsx", desc: "AI profile management (model + thinking presets)" },
{ file: "app-store.ts", desc: "Central Zustand state management" },
{
file: "context-view.tsx",
desc: "Context file manager for AI context",
},
{
file: "terminal-view.tsx",
desc: "Integrated terminal with splits and tabs",
},
{
file: "profiles-view.tsx",
desc: "AI profile management (model + thinking presets)",
},
{
file: "app-store.ts",
desc: "Central Zustand state management",
},
].map((item) => (
<div key={item.file} className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50">
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">{item.file}</code>
<span className="text-xs text-muted-foreground">{item.desc}</span>
<div
key={item.file}
className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50"
>
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">
{item.file}
</code>
<span className="text-xs text-muted-foreground">
{item.desc}
</span>
</div>
))}
</div>
@@ -356,21 +445,45 @@ export function WikiView() {
icon: Settings,
content: (
<div className="space-y-3">
<p>Automaker stores project configuration in the <code className="px-1 py-0.5 bg-muted rounded text-xs">.automaker/</code> directory:</p>
<p>
Automaker stores project configuration in the{" "}
<code className="px-1 py-0.5 bg-muted rounded text-xs">
.automaker/
</code>{" "}
directory:
</p>
<div className="grid gap-2 mt-4">
{[
{ file: "app_spec.txt", desc: "Project specification describing your app for AI context" },
{ file: "context/", desc: "Additional context files (docs, examples) for AI" },
{ file: "features/", desc: "Feature definitions with descriptions and steps" },
{
file: "app_spec.txt",
desc: "Project specification describing your app for AI context",
},
{
file: "context/",
desc: "Additional context files (docs, examples) for AI",
},
{
file: "features/",
desc: "Feature definitions with descriptions and steps",
},
].map((item) => (
<div key={item.file} className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50">
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">{item.file}</code>
<span className="text-xs text-muted-foreground">{item.desc}</span>
<div
key={item.file}
className="flex items-center gap-3 p-2 rounded bg-muted/30 border border-border/50"
>
<code className="text-xs font-mono text-brand-400 bg-brand-500/10 px-2 py-0.5 rounded">
{item.file}
</code>
<span className="text-xs text-muted-foreground">
{item.desc}
</span>
</div>
))}
</div>
<div className="mt-4 p-3 rounded-lg bg-muted/30 border border-border/50">
<p className="text-sm text-foreground font-medium mb-2">Tip: App Spec Best Practices</p>
<p className="text-sm text-foreground font-medium mb-2">
Tip: App Spec Best Practices
</p>
<ul className="list-disc list-inside space-y-1 text-xs text-muted-foreground">
<li>Include your tech stack and key dependencies</li>
<li>Describe the project structure and conventions</li>
@@ -391,39 +504,68 @@ export function WikiView() {
<ol className="list-decimal list-inside space-y-4 ml-2 mt-4">
<li className="text-foreground">
<strong>Create or Open a Project</strong>
<p className="text-muted-foreground ml-5 mt-1">Use the sidebar to create a new project or open an existing folder</p>
<p className="text-muted-foreground ml-5 mt-1">
Use the sidebar to create a new project or open an existing
folder
</p>
</li>
<li className="text-foreground">
<strong>Write an App Spec</strong>
<p className="text-muted-foreground ml-5 mt-1">Go to Spec Editor and describe your project. This helps AI understand your codebase.</p>
<p className="text-muted-foreground ml-5 mt-1">
Go to Spec Editor and describe your project. This helps AI
understand your codebase.
</p>
</li>
<li className="text-foreground">
<strong>Add Context (Optional)</strong>
<p className="text-muted-foreground ml-5 mt-1">Add relevant documentation or examples to the Context view for better AI results</p>
<p className="text-muted-foreground ml-5 mt-1">
Add relevant documentation or examples to the Context view for
better AI results
</p>
</li>
<li className="text-foreground">
<strong>Create Features</strong>
<p className="text-muted-foreground ml-5 mt-1">Add feature cards to your Kanban board with clear descriptions and implementation steps</p>
<p className="text-muted-foreground ml-5 mt-1">
Add feature cards to your Kanban board with clear descriptions
and implementation steps
</p>
</li>
<li className="text-foreground">
<strong>Configure AI Profile</strong>
<p className="text-muted-foreground ml-5 mt-1">Choose an AI profile or customize model/thinking settings per feature</p>
<p className="text-muted-foreground ml-5 mt-1">
Choose an AI profile or customize model/thinking settings per
feature
</p>
</li>
<li className="text-foreground">
<strong>Start Implementation</strong>
<p className="text-muted-foreground ml-5 mt-1">Drag features to "In Progress" or enable auto mode to let AI work</p>
<p className="text-muted-foreground ml-5 mt-1">
Drag features to "In Progress" or enable auto mode to let AI
work
</p>
</li>
<li className="text-foreground">
<strong>Review and Verify</strong>
<p className="text-muted-foreground ml-5 mt-1">Check completed features, review changes, and mark as verified</p>
<p className="text-muted-foreground ml-5 mt-1">
Check completed features, review changes, and mark as verified
</p>
</li>
</ol>
<div className="mt-6 p-4 rounded-lg bg-brand-500/10 border border-brand-500/20">
<p className="text-brand-400 text-sm font-medium mb-2">Pro Tips:</p>
<ul className="list-disc list-inside space-y-1 text-xs text-brand-400/80">
<li>Use keyboard shortcuts for faster navigation (press <code className="px-1 py-0.5 bg-brand-500/20 rounded">?</code> to see all)</li>
<li>Enable git worktree isolation for parallel feature development</li>
<li>Start with "Quick Edit" profile for simple tasks, use "Heavy Task" for complex work</li>
<li>
Use keyboard shortcuts for faster navigation (press{" "}
<code className="px-1 py-0.5 bg-brand-500/20 rounded">?</code>{" "}
to see all)
</li>
<li>
Enable git worktree isolation for parallel feature development
</li>
<li>
Start with "Quick Edit" profile for simple tasks, use "Heavy
Task" for complex work
</li>
<li>Keep your app spec up to date as your project evolves</li>
</ul>
</div>

View File

@@ -779,7 +779,7 @@ test.describe("Worktree Integration Tests", () => {
expect(featureData.worktreePath).toBeUndefined();
});
test("should store branch name when adding feature with new branch (worktree created at execution)", async ({
test("should store branch name when adding feature with new branch (worktree created when adding feature)", async ({
page,
}) => {
await setupProjectWithPath(page, testRepo.path);
@@ -788,7 +788,7 @@ test.describe("Worktree Integration Tests", () => {
await waitForBoardView(page);
// Use a branch name that doesn't exist yet
// Note: Worktrees are now created at execution time, not when adding to backlog
// Note: Worktrees are now created when features are added/edited, not at execution time
const branchName = "feature/auto-create-worktree";
// Verify branch does NOT exist before we create the feature
@@ -799,20 +799,28 @@ test.describe("Worktree Integration Tests", () => {
await clickAddFeature(page);
// Fill in the feature details with the new branch
await fillAddFeatureDialog(page, "Feature that should auto-create worktree", {
branch: branchName,
category: "Testing",
});
await fillAddFeatureDialog(
page,
"Feature that should auto-create worktree",
{
branch: branchName,
category: "Testing",
}
);
// Confirm
await confirmAddFeature(page);
// Wait for feature to be saved
await page.waitForTimeout(1000);
// Wait for feature to be saved and worktree to be created
await page.waitForTimeout(2000);
// Verify branch was NOT created when adding feature (created at execution time)
// Verify branch WAS created when adding feature (worktrees are created when features are added/edited)
const branchesAfter = await listBranches(testRepo.path);
expect(branchesAfter).not.toContain(branchName);
expect(branchesAfter).toContain(branchName);
// Verify worktree was created
const worktreePath = getWorktreePath(testRepo.path, branchName);
expect(fs.existsSync(worktreePath)).toBe(true);
// Verify feature was created with correct branch name stored
const featuresDir = path.join(testRepo.path, ".automaker", "features");
@@ -831,13 +839,9 @@ test.describe("Worktree Integration Tests", () => {
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Verify branch name is stored
expect(featureData.branchName).toBe(branchName);
// Verify worktreePath is NOT set (worktrees are created at execution time)
expect(featureData.worktreePath).toBeUndefined();
// Verify feature is in backlog status
expect(featureData.status).toBe("backlog");
});
@@ -896,7 +900,7 @@ test.describe("Worktree Integration Tests", () => {
let featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Verify feature was created with the branch name stored
expect(featureData.branchName).toBe(branchName);
// Verify worktreePath is NOT set (worktrees are created at execution time, not when adding)
@@ -1080,7 +1084,9 @@ test.describe("Worktree Integration Tests", () => {
// When a worktree is selected, "Use current selected branch" should be selected
// and the branch name should be shown in the label
const currentBranchLabel = page.locator('label[for="feature-current"]');
await expect(currentBranchLabel).toContainText(branchName, { timeout: 5000 });
await expect(currentBranchLabel).toContainText(branchName, {
timeout: 5000,
});
// Close dialog
await page.keyboard.press("Escape");
@@ -1271,11 +1277,7 @@ test.describe("Worktree Integration Tests", () => {
expect(featureDir).toBeDefined();
// Read the feature data
const featureFilePath = path.join(
featuresDir,
featureDir!,
"feature.json"
);
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(featureData.status).toBe("backlog");
@@ -1292,9 +1294,7 @@ test.describe("Worktree Integration Tests", () => {
// Wait for the feature to move to in_progress column
await expect(async () => {
const updatedData = JSON.parse(
fs.readFileSync(featureFilePath, "utf-8")
);
const updatedData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(updatedData.status).toBe("in_progress");
}).toPass({ timeout: 10000 });
@@ -1907,7 +1907,10 @@ test.describe("Worktree Integration Tests", () => {
await apiCreateWorktree(page, testRepo.path, branchName);
// Add a file and commit in the worktree
fs.writeFileSync(path.join(worktreePath, "merge-file.txt"), "merge content");
fs.writeFileSync(
path.join(worktreePath, "merge-file.txt"),
"merge content"
);
await execAsync("git add merge-file.txt", { cwd: worktreePath });
await execAsync('git commit -m "Add file for merge test"', {
cwd: worktreePath,
@@ -2061,9 +2064,9 @@ test.describe("Worktree Integration Tests", () => {
// Verify the worktree has the file from develop
const worktreePath = getWorktreePath(testRepo.path, "feature/from-develop");
expect(
fs.existsSync(path.join(worktreePath, "develop-only.txt"))
).toBe(true);
expect(fs.existsSync(path.join(worktreePath, "develop-only.txt"))).toBe(
true
);
const content = fs.readFileSync(
path.join(worktreePath, "develop-only.txt"),
"utf-8"
@@ -2096,10 +2099,9 @@ test.describe("Worktree Integration Tests", () => {
// Verify the worktree starts from the same commit as main
const worktreePath = getWorktreePath(testRepo.path, "feature/from-head");
const { stdout: worktreeHash } = await execAsync(
"git rev-parse HEAD~0",
{ cwd: worktreePath }
);
const { stdout: worktreeHash } = await execAsync("git rev-parse HEAD~0", {
cwd: worktreePath,
});
// The worktree's initial commit should be the same as main's HEAD
// (Since it was just created, we check the parent commit)
@@ -2391,15 +2393,15 @@ test.describe("Worktree Integration Tests", () => {
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
// Initially, the feature should be on main or have no branch set
expect(
!featureData.branchName || featureData.branchName === "main"
).toBe(true);
expect(!featureData.branchName || featureData.branchName === "main").toBe(
true
);
// The new branch we want to assign
const newBranchName = "feature/edited-branch";
const expectedWorktreePath = getWorktreePath(testRepo.path, newBranchName);
// Verify worktree does NOT exist before editing (worktrees are created at execution time)
// Verify worktree does NOT exist before editing
expect(fs.existsSync(expectedWorktreePath)).toBe(false);
// Find and click the edit button on the feature card
@@ -2424,7 +2426,7 @@ test.describe("Worktree Integration Tests", () => {
await page.waitForTimeout(300);
// Type the new branch name
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill(newBranchName);
// Press Enter to select/create the branch
@@ -2435,22 +2437,19 @@ test.describe("Worktree Integration Tests", () => {
const saveButton = page.locator('[data-testid="confirm-edit-feature"]');
await saveButton.click();
// Wait for the dialog to close
// Wait for the dialog to close and worktree to be created
await page.waitForTimeout(2000);
// Verify worktree was NOT created during editing (worktrees are created at execution time)
expect(fs.existsSync(expectedWorktreePath)).toBe(false);
// Verify worktree WAS created during editing (worktrees are now created when features are added/edited)
expect(fs.existsSync(expectedWorktreePath)).toBe(true);
// Verify branch was NOT created (created at execution time)
// Verify branch WAS created (worktrees are created when features are added/edited)
const branches = await listBranches(testRepo.path);
expect(branches).not.toContain(newBranchName);
expect(branches).toContain(newBranchName);
// Verify feature was updated with correct branchName only
// Note: worktreePath is no longer stored - worktrees are created server-side at execution time
// Verify feature was updated with correct branchName
featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(featureData.branchName).toBe(newBranchName);
// worktreePath should not exist in the feature data
expect(featureData.worktreePath).toBeUndefined();
});
test("should not create worktree when editing a feature and selecting main branch", async ({
@@ -2518,7 +2517,7 @@ test.describe("Worktree Integration Tests", () => {
await page.waitForTimeout(300);
// Type "main" to change to main branch
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill("main");
await commandInput.press("Enter");
await page.waitForTimeout(200);
@@ -2576,7 +2575,7 @@ test.describe("Worktree Integration Tests", () => {
await branchInput.click();
await page.waitForTimeout(300);
const commandInput = page.locator('[cmdk-input]');
const commandInput = page.locator("[cmdk-input]");
await commandInput.fill(existingBranch);
await commandInput.press("Enter");
await page.waitForTimeout(200);