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:
Kacper
2025-12-10 13:41:52 +01:00
parent 02b3275460
commit 7ab2aaaa23
13 changed files with 1190 additions and 3939 deletions

View File

@@ -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(() => {

View File

@@ -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"
/>
) : (

View File

@@ -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

View File

@@ -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);

View File

@@ -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,
}),
}

View File

@@ -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;