mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
Merge branch 'main' into refactor/frontend
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/{id}/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/{id}/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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user