mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +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:
@@ -46,8 +46,18 @@ class AutoModeService {
|
||||
/**
|
||||
* Setup worktree for a feature
|
||||
* Creates an isolated git worktree where the agent can work
|
||||
* @param {Object} feature - The feature object
|
||||
* @param {string} projectPath - Path to the project
|
||||
* @param {Function} sendToRenderer - Function to send events to the renderer
|
||||
* @param {boolean} useWorktreesEnabled - Whether worktrees are enabled in settings (default: false)
|
||||
*/
|
||||
async setupWorktreeForFeature(feature, projectPath, sendToRenderer) {
|
||||
async setupWorktreeForFeature(feature, projectPath, sendToRenderer, useWorktreesEnabled = false) {
|
||||
// If worktrees are disabled in settings, skip entirely
|
||||
if (!useWorktreesEnabled) {
|
||||
console.log(`[AutoMode] Worktrees disabled in settings, working directly on main project`);
|
||||
return { useWorktree: false, workPath: projectPath };
|
||||
}
|
||||
|
||||
// Check if worktrees are enabled (project must be a git repo)
|
||||
const isGit = await worktreeManager.isGitRepo(projectPath);
|
||||
if (!isGit) {
|
||||
@@ -164,14 +174,18 @@ class AutoModeService {
|
||||
|
||||
/**
|
||||
* Run a specific feature by ID
|
||||
* @param {string} projectPath - Path to the project
|
||||
* @param {string} featureId - ID of the feature to run
|
||||
* @param {Function} sendToRenderer - Function to send events to renderer
|
||||
* @param {boolean} useWorktrees - Whether to use git worktree isolation (default: false)
|
||||
*/
|
||||
async runFeature({ projectPath, featureId, sendToRenderer }) {
|
||||
async runFeature({ projectPath, featureId, sendToRenderer, useWorktrees = false }) {
|
||||
// Check if this specific feature is already running
|
||||
if (this.runningFeatures.has(featureId)) {
|
||||
throw new Error(`Feature ${featureId} is already running`);
|
||||
}
|
||||
|
||||
console.log(`[AutoMode] Running specific feature: ${featureId}`);
|
||||
console.log(`[AutoMode] Running specific feature: ${featureId} (worktrees: ${useWorktrees})`);
|
||||
|
||||
// Register this feature as running
|
||||
const execution = this.createExecutionContext(featureId);
|
||||
@@ -190,8 +204,8 @@ class AutoModeService {
|
||||
|
||||
console.log(`[AutoMode] Running feature: ${feature.description}`);
|
||||
|
||||
// Setup worktree for isolated work
|
||||
const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer);
|
||||
// Setup worktree for isolated work (if enabled)
|
||||
const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer, useWorktrees);
|
||||
execution.worktreePath = worktreeSetup.workPath;
|
||||
execution.branchName = worktreeSetup.branchName;
|
||||
|
||||
@@ -621,8 +635,12 @@ class AutoModeService {
|
||||
|
||||
/**
|
||||
* Start a feature asynchronously (similar to drag operation)
|
||||
* @param {Object} feature - The feature to start
|
||||
* @param {string} projectPath - Path to the project
|
||||
* @param {Function} sendToRenderer - Function to send events to renderer
|
||||
* @param {boolean} useWorktrees - Whether to use git worktree isolation (default: false)
|
||||
*/
|
||||
async startFeatureAsync(feature, projectPath, sendToRenderer) {
|
||||
async startFeatureAsync(feature, projectPath, sendToRenderer, useWorktrees = false) {
|
||||
const featureId = feature.id;
|
||||
|
||||
// Skip if already running
|
||||
@@ -633,7 +651,7 @@ class AutoModeService {
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[AutoMode] Starting feature: ${feature.description.slice(0, 50)}...`
|
||||
`[AutoMode] Starting feature: ${feature.description.slice(0, 50)}... (worktrees: ${useWorktrees})`
|
||||
);
|
||||
|
||||
// Register this feature as running
|
||||
@@ -642,8 +660,8 @@ class AutoModeService {
|
||||
execution.sendToRenderer = sendToRenderer;
|
||||
this.runningFeatures.set(featureId, execution);
|
||||
|
||||
// Setup worktree for isolated work
|
||||
const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer);
|
||||
// Setup worktree for isolated work (if enabled)
|
||||
const worktreeSetup = await this.setupWorktreeForFeature(feature, projectPath, sendToRenderer, useWorktrees);
|
||||
execution.worktreePath = worktreeSetup.workPath;
|
||||
execution.branchName = worktreeSetup.branchName;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
||||
const fs = require("fs/promises");
|
||||
const agentService = require("./agent-service");
|
||||
const autoModeService = require("./auto-mode-service");
|
||||
const worktreeManager = require("./services/worktree-manager");
|
||||
|
||||
let mainWindow = null;
|
||||
|
||||
@@ -468,7 +469,7 @@ ipcMain.handle("auto-mode:status", () => {
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"auto-mode:run-feature",
|
||||
async (_, { projectPath, featureId }) => {
|
||||
async (_, { projectPath, featureId, useWorktrees = false }) => {
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
@@ -480,6 +481,7 @@ ipcMain.handle(
|
||||
projectPath,
|
||||
featureId,
|
||||
sendToRenderer,
|
||||
useWorktrees,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:run-feature error:", error);
|
||||
@@ -934,3 +936,27 @@ ipcMain.handle("worktree:get-file-diff", async (_, { projectPath, featureId, fil
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get file diffs for the main project (non-worktree)
|
||||
*/
|
||||
ipcMain.handle("git:get-diffs", async (_, { projectPath }) => {
|
||||
try {
|
||||
return await worktreeManager.getFileDiffs(projectPath);
|
||||
} catch (error) {
|
||||
console.error("[IPC] git:get-diffs error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get diff for a specific file in the main project (non-worktree)
|
||||
*/
|
||||
ipcMain.handle("git:get-file-diff", async (_, { projectPath, filePath }) => {
|
||||
try {
|
||||
return await worktreeManager.getFileDiff(projectPath, filePath);
|
||||
} catch (error) {
|
||||
console.error("[IPC] git:get-file-diff error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -97,8 +97,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
status: () => ipcRenderer.invoke("auto-mode:status"),
|
||||
|
||||
// Run a specific feature
|
||||
runFeature: (projectPath, featureId) =>
|
||||
ipcRenderer.invoke("auto-mode:run-feature", { projectPath, featureId }),
|
||||
runFeature: (projectPath, featureId, useWorktrees) =>
|
||||
ipcRenderer.invoke("auto-mode:run-feature", { projectPath, featureId, useWorktrees }),
|
||||
|
||||
// Verify a specific feature by running its tests
|
||||
verifyFeature: (projectPath, featureId) =>
|
||||
@@ -189,6 +189,17 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
getFileDiff: (projectPath, featureId, filePath) =>
|
||||
ipcRenderer.invoke("worktree:get-file-diff", { projectPath, featureId, filePath }),
|
||||
},
|
||||
|
||||
// Git Operations APIs (for non-worktree operations)
|
||||
git: {
|
||||
// Get file diffs for the main project
|
||||
getDiffs: (projectPath) =>
|
||||
ipcRenderer.invoke("git:get-diffs", { projectPath }),
|
||||
|
||||
// Get diff for a specific file in the main project
|
||||
getFileDiff: (projectPath, filePath) =>
|
||||
ipcRenderer.invoke("git:get-file-diff", { projectPath, filePath }),
|
||||
},
|
||||
});
|
||||
|
||||
// Also expose a flag to detect if we're in Electron
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
Sparkles,
|
||||
Wand2,
|
||||
LayoutGrid,
|
||||
Layers,
|
||||
FolderOpen,
|
||||
FileText,
|
||||
List,
|
||||
Cpu,
|
||||
Search,
|
||||
Share2,
|
||||
Trash2,
|
||||
BarChart3,
|
||||
Settings,
|
||||
PanelLeftClose,
|
||||
PanelLeft,
|
||||
Home,
|
||||
LogOut,
|
||||
User,
|
||||
CreditCard,
|
||||
} from "lucide-react";
|
||||
|
||||
interface AppSidebarProps {
|
||||
user: any;
|
||||
creditsBalance: number | null;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
href: string;
|
||||
icon: any;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface NavSection {
|
||||
label?: string;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
export function AppSidebar({ user, creditsBalance }: AppSidebarProps) {
|
||||
const pathname = usePathname();
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const userMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
userMenuRef.current &&
|
||||
!userMenuRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setUserMenuOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (userMenuOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [userMenuOpen]);
|
||||
|
||||
const navSections: NavSection[] = [
|
||||
{
|
||||
items: [
|
||||
{ href: "/generate", icon: Home, label: "Overview" },
|
||||
{ href: "/generate/canvas", icon: Wand2, label: "Canvas" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Content",
|
||||
items: [
|
||||
{ href: "/generate/gallery", icon: LayoutGrid, label: "Gallery" },
|
||||
{ href: "/generate/collections", icon: Layers, label: "Collections" },
|
||||
{ href: "/generate/projects", icon: FolderOpen, label: "Projects" },
|
||||
{ href: "/generate/prompts", icon: FileText, label: "Prompts" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Tools",
|
||||
items: [
|
||||
{ href: "/generate/batch", icon: List, label: "Batch" },
|
||||
{ href: "/generate/models", icon: Cpu, label: "Models" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Manage",
|
||||
items: [
|
||||
{ href: "/generate/shared", icon: Share2, label: "Shared" },
|
||||
{ href: "/generate/trash", icon: Trash2, label: "Trash" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const isActiveRoute = (href: string) => {
|
||||
if (href === "/generate") {
|
||||
return pathname === "/generate";
|
||||
}
|
||||
return pathname?.startsWith(href);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`${
|
||||
sidebarCollapsed ? "w-16" : "w-16 lg:w-60"
|
||||
} flex-shrink-0 border-r border-white/10 bg-zinc-950/50 backdrop-blur-md flex flex-col z-30 transition-all duration-300 relative`}
|
||||
data-testid="left-sidebar"
|
||||
data-collapsed={sidebarCollapsed}
|
||||
>
|
||||
{/* Floating Collapse Toggle Button */}
|
||||
<button
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
className="hidden lg:flex absolute top-20 -right-3 z-50 items-center justify-center w-6 h-6 rounded-full bg-zinc-800 border border-white/10 text-zinc-400 hover:text-white hover:bg-zinc-700 hover:border-white/20 transition-all shadow-lg"
|
||||
data-testid="sidebar-collapse-button"
|
||||
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<PanelLeft className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<PanelLeftClose className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Logo */}
|
||||
<div className={`h-16 flex items-center border-b border-zinc-800 flex-shrink-0 ${
|
||||
sidebarCollapsed ? "justify-center" : "justify-center lg:justify-start lg:px-6"
|
||||
}`}>
|
||||
<Link href="/generate" className="flex items-center">
|
||||
<div className="relative flex items-center justify-center w-8 h-8 bg-gradient-to-br from-brand-500 to-purple-600 rounded-lg shadow-lg shadow-brand-500/20 group cursor-pointer">
|
||||
<Sparkles className="text-white w-5 h-5 group-hover:rotate-12 transition-transform" />
|
||||
</div>
|
||||
<span
|
||||
className={`ml-3 font-bold text-white text-base tracking-tight ${
|
||||
sidebarCollapsed ? "hidden" : "hidden lg:block"
|
||||
}`}
|
||||
>
|
||||
Image<span className="text-brand-500">Studio</span>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Nav Items - Scrollable */}
|
||||
<nav className="flex-1 overflow-y-auto px-2 mt-2 pb-2">
|
||||
{navSections.map((section, sectionIdx) => (
|
||||
<div key={sectionIdx} className={sectionIdx > 0 ? "mt-6" : ""}>
|
||||
{/* Section Label */}
|
||||
{section.label && !sidebarCollapsed && (
|
||||
<div className="hidden lg:block px-4 mb-2">
|
||||
<span className="text-[10px] font-semibold text-zinc-500 uppercase tracking-wider">
|
||||
{section.label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{section.label && sidebarCollapsed && (
|
||||
<div className="h-px bg-zinc-800 mx-2 mb-2"></div>
|
||||
)}
|
||||
|
||||
{/* Nav Items */}
|
||||
<div className="space-y-1">
|
||||
{section.items.map((item) => {
|
||||
const isActive = isActiveRoute(item.href);
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`group flex items-center px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all ${
|
||||
isActive
|
||||
? "bg-white/5 text-white border border-white/10"
|
||||
: "text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
title={sidebarCollapsed ? item.label : undefined}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
|
||||
)}
|
||||
<Icon
|
||||
className={`w-4 h-4 flex-shrink-0 transition-colors ${
|
||||
isActive
|
||||
? "text-brand-500"
|
||||
: "group-hover:text-brand-400"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`ml-2.5 font-medium text-sm ${
|
||||
sidebarCollapsed ? "hidden" : "hidden lg:block"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{/* Tooltip for collapsed state */}
|
||||
{sidebarCollapsed && (
|
||||
<span
|
||||
className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700"
|
||||
data-testid={`sidebar-tooltip-${item.label.toLowerCase()}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section - User / Settings */}
|
||||
<div className="border-t border-zinc-800 bg-zinc-900/50 flex-shrink-0">
|
||||
{/* Usage & Settings Links */}
|
||||
<div className="p-2 space-y-1">
|
||||
<Link
|
||||
href="/generate/usage"
|
||||
className={`group flex items-center px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all ${
|
||||
isActiveRoute("/generate/usage")
|
||||
? "bg-white/5 text-white border border-white/10"
|
||||
: "text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
title={sidebarCollapsed ? "Usage" : undefined}
|
||||
>
|
||||
{isActiveRoute("/generate/usage") && (
|
||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
|
||||
)}
|
||||
<BarChart3
|
||||
className={`w-4 h-4 flex-shrink-0 transition-colors ${
|
||||
isActiveRoute("/generate/usage")
|
||||
? "text-brand-500"
|
||||
: "group-hover:text-brand-400"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`ml-2.5 font-medium text-sm ${
|
||||
sidebarCollapsed ? "hidden" : "hidden lg:block"
|
||||
}`}
|
||||
>
|
||||
Usage
|
||||
</span>
|
||||
{sidebarCollapsed && (
|
||||
<span className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700">
|
||||
Usage
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/generate/settings"
|
||||
className={`group flex items-center px-2 lg:px-3 py-2.5 rounded-lg relative overflow-hidden transition-all ${
|
||||
isActiveRoute("/generate/settings")
|
||||
? "bg-white/5 text-white border border-white/10"
|
||||
: "text-zinc-400 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
title={sidebarCollapsed ? "Settings" : undefined}
|
||||
>
|
||||
{isActiveRoute("/generate/settings") && (
|
||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-brand-500 rounded-l-md"></div>
|
||||
)}
|
||||
<Settings
|
||||
className={`w-4 h-4 flex-shrink-0 transition-colors ${
|
||||
isActiveRoute("/generate/settings")
|
||||
? "text-brand-500"
|
||||
: "group-hover:text-brand-400"
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`ml-2.5 font-medium text-sm ${
|
||||
sidebarCollapsed ? "hidden" : "hidden lg:block"
|
||||
}`}
|
||||
>
|
||||
Settings
|
||||
</span>
|
||||
{sidebarCollapsed && (
|
||||
<span className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700">
|
||||
Settings
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Credits Display */}
|
||||
{!sidebarCollapsed && (
|
||||
<Link href="/generate/usage" className="hidden lg:block mx-3 mb-3">
|
||||
<div className="p-2.5 bg-white/5 backdrop-blur-sm rounded-lg border border-white/10 hover:bg-white/10 hover:border-white/20 transition-all cursor-pointer">
|
||||
<div className="flex justify-between text-[11px] font-medium text-zinc-400 mb-1.5">
|
||||
<span>Credits</span>
|
||||
<span className="text-white" data-testid="credits-sidebar-balance">
|
||||
{creditsBalance !== null ? creditsBalance : "..."} / 1000
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-zinc-800 rounded-full h-1 overflow-hidden">
|
||||
<div
|
||||
className="bg-gradient-to-r from-brand-500 to-purple-500 h-1 rounded-full"
|
||||
style={{
|
||||
width: `${
|
||||
creditsBalance !== null
|
||||
? Math.min((creditsBalance / 1000) * 100, 100)
|
||||
: 30
|
||||
}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* User Profile */}
|
||||
<div className="p-3 border-t border-zinc-800" ref={userMenuRef}>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
className={`flex items-center p-1.5 rounded-lg transition-colors group relative w-full hover:bg-white/5 ${
|
||||
sidebarCollapsed ? "justify-center" : "lg:space-x-2.5"
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={
|
||||
user?.avatarUrl ||
|
||||
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=100&q=80"
|
||||
}
|
||||
alt="User"
|
||||
className="w-8 h-8 rounded-full border border-zinc-600"
|
||||
/>
|
||||
<div className="absolute bottom-0 right-0 w-2 h-2 bg-green-500 border-2 border-zinc-900 rounded-full"></div>
|
||||
</div>
|
||||
<div
|
||||
className={`overflow-hidden ${
|
||||
sidebarCollapsed ? "hidden" : "hidden lg:block"
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs font-medium text-white truncate">
|
||||
{user ? user.name : "Guest"}
|
||||
</p>
|
||||
<p className="text-[10px] text-zinc-500 truncate">
|
||||
{user ? "Pro Account" : "Guest"}
|
||||
</p>
|
||||
</div>
|
||||
{/* Tooltip for user when collapsed */}
|
||||
{sidebarCollapsed && (
|
||||
<span
|
||||
className="absolute left-full ml-2 px-2 py-1 bg-zinc-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 border border-zinc-700"
|
||||
data-testid="sidebar-tooltip-user"
|
||||
>
|
||||
{user ? user.name : "Guest"}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{userMenuOpen && (
|
||||
<div
|
||||
className={`absolute bottom-full mb-2 bg-zinc-800 border border-zinc-700 rounded-xl shadow-lg overflow-hidden z-50 ${
|
||||
sidebarCollapsed ? "left-0" : "left-0 right-0"
|
||||
}`}
|
||||
>
|
||||
<div className="py-2">
|
||||
<Link
|
||||
href="/generate/settings"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-700/50 hover:text-white transition-colors"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-3" />
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/generate/usage"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-700/50 hover:text-white transition-colors"
|
||||
>
|
||||
<BarChart3 className="w-4 h-4 mr-3" />
|
||||
<span>Usage</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard/profile"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-700/50 hover:text-white transition-colors"
|
||||
>
|
||||
<User className="w-4 h-4 mr-3" />
|
||||
<span>Profile</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard/billing"
|
||||
onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-700/50 hover:text-white transition-colors"
|
||||
>
|
||||
<CreditCard className="w-4 h-4 mr-3" />
|
||||
<span>Billing</span>
|
||||
</Link>
|
||||
<div className="border-t border-zinc-700 my-2"></div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
// Add logout logic here
|
||||
window.location.href = "/api/auth/logout";
|
||||
}}
|
||||
className="flex items-center px-4 py-2 text-sm text-red-400 hover:bg-zinc-700/50 hover:text-red-300 transition-colors w-full text-left"
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-3" />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
2590
app/example/page.tsx
2590
app/example/page.tsx
File diff suppressed because it is too large
Load Diff
@@ -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