mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat(worktree): enhance worktree management and git diff functionality
- Integrated git worktree isolation for feature execution, allowing agents to work in isolated branches. - Added GitDiffPanel component to visualize changes in both worktree and main project contexts. - Updated AutoModeService and IPC handlers to support worktree settings. - Implemented Git API for non-worktree operations, enabling file diff retrieval for the main project. - Enhanced UI components to reflect worktree settings and improve user experience. These changes provide a more robust and flexible environment for feature development and testing.
This commit is contained in:
@@ -25,6 +25,8 @@ interface GitDiffPanelProps {
|
||||
className?: string;
|
||||
/** Whether to show the panel in a compact/minimized state initially */
|
||||
compact?: boolean;
|
||||
/** Whether worktrees are enabled - if false, shows diffs from main project */
|
||||
useWorktrees?: boolean;
|
||||
}
|
||||
|
||||
interface ParsedDiffHunk {
|
||||
@@ -334,6 +336,7 @@ export function GitDiffPanel({
|
||||
featureId,
|
||||
className,
|
||||
compact = true,
|
||||
useWorktrees = false,
|
||||
}: GitDiffPanelProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(!compact);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -347,23 +350,38 @@ export function GitDiffPanel({
|
||||
setError(null);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.getDiffs) {
|
||||
throw new Error("Worktree API not available");
|
||||
}
|
||||
|
||||
const result = await api.worktree.getDiffs(projectPath, featureId);
|
||||
if (result.success) {
|
||||
setFiles(result.files || []);
|
||||
setDiffContent(result.diff || "");
|
||||
// Use worktree API if worktrees are enabled, otherwise use git API for main project
|
||||
if (useWorktrees) {
|
||||
if (!api?.worktree?.getDiffs) {
|
||||
throw new Error("Worktree API not available");
|
||||
}
|
||||
const result = await api.worktree.getDiffs(projectPath, featureId);
|
||||
if (result.success) {
|
||||
setFiles(result.files || []);
|
||||
setDiffContent(result.diff || "");
|
||||
} else {
|
||||
setError(result.error || "Failed to load diffs");
|
||||
}
|
||||
} else {
|
||||
setError(result.error || "Failed to load diffs");
|
||||
// Use git API for main project diffs
|
||||
if (!api?.git?.getDiffs) {
|
||||
throw new Error("Git API not available");
|
||||
}
|
||||
const result = await api.git.getDiffs(projectPath);
|
||||
if (result.success) {
|
||||
setFiles(result.files || []);
|
||||
setDiffContent(result.diff || "");
|
||||
} else {
|
||||
setError(result.error || "Failed to load diffs");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load diffs");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [projectPath, featureId]);
|
||||
}, [projectPath, featureId, useWorktrees]);
|
||||
|
||||
// Load diffs when expanded
|
||||
useEffect(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Loader2, List, FileText, GitBranch } from "lucide-react";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { LogViewer } from "@/components/ui/log-viewer";
|
||||
import { GitDiffPanel } from "@/components/ui/git-diff-panel";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import type { AutoModeEvent } from "@/types/electron";
|
||||
|
||||
interface AgentOutputModalProps {
|
||||
@@ -39,6 +40,7 @@ export function AgentOutputModal({
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const autoScrollRef = useRef(true);
|
||||
const projectPathRef = useRef<string>("");
|
||||
const useWorktrees = useAppStore((state) => state.useWorktrees);
|
||||
|
||||
// Auto-scroll to bottom when output changes
|
||||
useEffect(() => {
|
||||
@@ -303,6 +305,7 @@ export function AgentOutputModal({
|
||||
projectPath={projectPath}
|
||||
featureId={featureId}
|
||||
compact={false}
|
||||
useWorktrees={useWorktrees}
|
||||
className="border-0 rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -184,6 +184,7 @@ export function BoardView() {
|
||||
maxConcurrency,
|
||||
setMaxConcurrency,
|
||||
defaultSkipTests,
|
||||
useWorktrees,
|
||||
aiProfiles,
|
||||
} = useAppStore();
|
||||
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
||||
@@ -849,7 +850,8 @@ export function BoardView() {
|
||||
// Call the API to run this specific feature by ID
|
||||
const result = await api.autoMode.runFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
feature.id,
|
||||
useWorktrees
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,14 +42,14 @@ export interface StatResult {
|
||||
}
|
||||
|
||||
// Auto Mode types - Import from electron.d.ts to avoid duplication
|
||||
import type { AutoModeEvent, ModelDefinition, ProviderStatus, WorktreeAPI, WorktreeInfo, WorktreeStatus, FileDiffsResult, FileDiffResult, FileStatus } from "@/types/electron";
|
||||
import type { AutoModeEvent, ModelDefinition, ProviderStatus, WorktreeAPI, GitAPI, WorktreeInfo, WorktreeStatus, FileDiffsResult, FileDiffResult, FileStatus } from "@/types/electron";
|
||||
|
||||
export interface AutoModeAPI {
|
||||
start: (projectPath: string, maxConcurrency?: number) => Promise<{ success: boolean; error?: string }>;
|
||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
||||
stopFeature: (featureId: string) => Promise<{ success: boolean; error?: string }>;
|
||||
status: () => Promise<{ success: boolean; isRunning?: boolean; currentFeatureId?: string | null; runningFeatures?: string[]; error?: string }>;
|
||||
runFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
runFeature: (projectPath: string, featureId: string, useWorktrees?: boolean) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
verifyFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
resumeFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
contextExists: (projectPath: string, featureId: string) => Promise<{ success: boolean; exists?: boolean; error?: string }>;
|
||||
@@ -129,6 +129,7 @@ export interface ElectronAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
worktree?: WorktreeAPI;
|
||||
git?: GitAPI;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -426,6 +427,9 @@ export const getElectronAPI = (): ElectronAPI => {
|
||||
|
||||
// Mock Worktree API
|
||||
worktree: createMockWorktreeAPI(),
|
||||
|
||||
// Mock Git API (for non-worktree operations)
|
||||
git: createMockGitAPI(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -495,6 +499,33 @@ function createMockWorktreeAPI(): WorktreeAPI {
|
||||
};
|
||||
}
|
||||
|
||||
// Mock Git API implementation (for non-worktree operations)
|
||||
function createMockGitAPI(): GitAPI {
|
||||
return {
|
||||
getDiffs: async (projectPath: string) => {
|
||||
console.log("[Mock] Getting git diffs for project:", { projectPath });
|
||||
return {
|
||||
success: true,
|
||||
diff: "diff --git a/src/feature.ts b/src/feature.ts\n+++ new file\n@@ -0,0 +1,10 @@\n+export function feature() {\n+ return 'hello';\n+}",
|
||||
files: [
|
||||
{ status: "A", path: "src/feature.ts", statusText: "Added" },
|
||||
{ status: "M", path: "README.md", statusText: "Modified" },
|
||||
],
|
||||
hasChanges: true,
|
||||
};
|
||||
},
|
||||
|
||||
getFileDiff: async (projectPath: string, filePath: string) => {
|
||||
console.log("[Mock] Getting git file diff:", { projectPath, filePath });
|
||||
return {
|
||||
success: true,
|
||||
diff: `diff --git a/${filePath} b/${filePath}\n+++ new file\n@@ -0,0 +1,5 @@\n+// New content`,
|
||||
filePath,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Mock Auto Mode state and implementation
|
||||
let mockAutoModeRunning = false;
|
||||
let mockRunningFeatures = new Set<string>(); // Track multiple concurrent feature verifications
|
||||
@@ -563,11 +594,12 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
||||
};
|
||||
},
|
||||
|
||||
runFeature: async (projectPath: string, featureId: string) => {
|
||||
runFeature: async (projectPath: string, featureId: string, useWorktrees?: boolean) => {
|
||||
if (mockRunningFeatures.has(featureId)) {
|
||||
return { success: false, error: `Feature ${featureId} is already running` };
|
||||
}
|
||||
|
||||
console.log(`[Mock] Running feature ${featureId} with useWorktrees: ${useWorktrees}`);
|
||||
mockRunningFeatures.add(featureId);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
|
||||
@@ -168,6 +168,9 @@ export interface AppState {
|
||||
// Feature Default Settings
|
||||
defaultSkipTests: boolean; // Default value for skip tests when creating new features
|
||||
|
||||
// Worktree Settings
|
||||
useWorktrees: boolean; // Whether to use git worktree isolation for features (default: false)
|
||||
|
||||
// AI Profiles
|
||||
aiProfiles: AIProfile[];
|
||||
}
|
||||
@@ -255,6 +258,9 @@ export interface AppActions {
|
||||
// Feature Default Settings actions
|
||||
setDefaultSkipTests: (skip: boolean) => void;
|
||||
|
||||
// Worktree Settings actions
|
||||
setUseWorktrees: (enabled: boolean) => void;
|
||||
|
||||
// AI Profile actions
|
||||
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
|
||||
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
|
||||
@@ -343,6 +349,7 @@ const initialState: AppState = {
|
||||
maxConcurrency: 3, // Default to 3 concurrent agents
|
||||
kanbanCardDetailLevel: "standard", // Default to standard detail level
|
||||
defaultSkipTests: false, // Default to TDD mode (tests enabled)
|
||||
useWorktrees: false, // Default to disabled (worktree feature is experimental)
|
||||
aiProfiles: DEFAULT_AI_PROFILES,
|
||||
};
|
||||
|
||||
@@ -672,6 +679,9 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
// Feature Default Settings actions
|
||||
setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }),
|
||||
|
||||
// Worktree Settings actions
|
||||
setUseWorktrees: (enabled) => set({ useWorktrees: enabled }),
|
||||
|
||||
// AI Profile actions
|
||||
addAIProfile: (profile) => {
|
||||
const id = `profile-${Date.now()}-${Math.random()
|
||||
@@ -721,6 +731,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
maxConcurrency: state.maxConcurrency,
|
||||
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
||||
defaultSkipTests: state.defaultSkipTests,
|
||||
useWorktrees: state.useWorktrees,
|
||||
aiProfiles: state.aiProfiles,
|
||||
}),
|
||||
}
|
||||
|
||||
13
app/src/types/electron.d.ts
vendored
13
app/src/types/electron.d.ts
vendored
@@ -229,7 +229,7 @@ export interface AutoModeAPI {
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
runFeature: (projectPath: string, featureId: string) => Promise<{
|
||||
runFeature: (projectPath: string, featureId: string, useWorktrees?: boolean) => Promise<{
|
||||
success: boolean;
|
||||
passes?: boolean;
|
||||
error?: string;
|
||||
@@ -386,6 +386,9 @@ export interface ElectronAPI {
|
||||
|
||||
// Worktree Management APIs
|
||||
worktree: WorktreeAPI;
|
||||
|
||||
// Git Operations APIs (for non-worktree operations)
|
||||
git: GitAPI;
|
||||
}
|
||||
|
||||
export interface WorktreeInfo {
|
||||
@@ -470,6 +473,14 @@ export interface WorktreeAPI {
|
||||
getFileDiff: (projectPath: string, featureId: string, filePath: string) => Promise<FileDiffResult>;
|
||||
}
|
||||
|
||||
export interface GitAPI {
|
||||
// Get diffs for the main project (not a worktree)
|
||||
getDiffs: (projectPath: string) => Promise<FileDiffsResult>;
|
||||
|
||||
// Get diff for a specific file in the main project
|
||||
getFileDiff: (projectPath: string, filePath: string) => Promise<FileDiffResult>;
|
||||
}
|
||||
|
||||
// Model definition type
|
||||
export interface ModelDefinition {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user