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

@@ -72,9 +72,9 @@
"description": "When agent finish work the cards is moved either to waiting approval or into verified one But mostly its include some type of summary at the end i want you to modify our prompts and ui so when its in both states we can see the feature summary of what was done / modified instead of relying on going to code editor to see what got changed etc.",
"steps": [],
"status": "verified",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-09T22:09:13.684Z",
"imagePaths": [],
"skipTests": true,
"model": "opus",
"thinkingLevel": "none"
},
@@ -84,9 +84,9 @@
"description": "When running new feature in skip automated testing once its got finished its moved to waiting approval for us to manual test it / follow up prompt. Once we are satisfied we can click commit button so ai agent can commit it work this is only hapening in this scenerio because if we have unchecked the skip automated testing its do it automaticly and commit already. But the issue is when its going to commit we move it to in progress state where we can use stop button and if user use that button its moved to backlog column and. that kinda break what we are doing becase we have no longer even abbility to move it back to waiting approval or to run commit button / follow up again so if user use manual one and stop the commit i want it to be again moved back to waiting approval state / column",
"steps": [],
"status": "verified",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-09T22:31:41.946Z",
"imagePaths": [],
"skipTests": true,
"model": "opus",
"thinkingLevel": "none"
},
@@ -102,9 +102,9 @@
"agent execute task with correct model "
],
"status": "verified",
"skipTests": false,
"imagePaths": [],
"startedAt": "2025-12-09T23:07:37.223Z",
"imagePaths": [],
"skipTests": false,
"summary": "Added model selection (Haiku/Sonnet/Opus) and thinking level (None/Low/Medium/High) controls to feature creation and edit dialogs. Modified: app-store.ts (added AgentModel and ThinkingLevel types), board-view.tsx (UI controls), feature-executor.js (dynamic model/thinking config), feature-loader.js (field persistence). Agent now executes with user-selected model and extended thinking settings.",
"model": "opus",
"thinkingLevel": "none"
@@ -115,9 +115,9 @@
"description": "I want you to refactor the add new feature modal there are to many settings going on and its hard / annoyig to navigate lets split the settings in modal into tabs \nprompt icon - prompt and category\ngear icon - model and thinking ( here i would also like to split somehow the claude with thinking and codex that dont use it )\ntest icon - skip automated testing and verification steps\n",
"steps": [],
"status": "verified",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-10T02:17:18.943Z",
"imagePaths": [],
"skipTests": true,
"summary": "Made model selection buttons compact. Removed descriptions and badges from cards, now shows short model names (Haiku, Sonnet, Opus, Max, Codex, Mini) in horizontal row. Full description available on hover. Modified: board-view.tsx (renderModelOptions function).",
"model": "opus",
"thinkingLevel": "high"
@@ -128,7 +128,7 @@
"description": "Make the add new feature modal widther ",
"steps": [],
"status": "verified",
"skipTests": true,
"startedAt": "2025-12-10T02:25:21.328Z",
"imagePaths": [
{
"id": "img-1765333063064-qygrbjul4",
@@ -137,7 +137,7 @@
"mimeType": "image/png"
}
],
"startedAt": "2025-12-10T02:25:21.328Z",
"skipTests": true,
"summary": "Increased dialog max-width from max-w-md/max-w-lg to max-w-2xl. Modified: app/src/components/ui/dialog.tsx. This makes the add new feature modal and all other dialogs wider (from 448-512px to 672px) for better content display.",
"model": "haiku",
"thinkingLevel": "none"
@@ -148,9 +148,9 @@
"description": "For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task",
"steps": [],
"status": "verified",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-10T02:40:43.706Z",
"imagePaths": [],
"skipTests": true,
"summary": "Kanban cards now render the agent info model badge using feature.model so the displayed model matches the one selected for the task.",
"model": "gpt-5.1-codex",
"thinkingLevel": "none"
@@ -161,7 +161,7 @@
"description": "describe the attached image do not change code",
"steps": [],
"status": "verified",
"skipTests": true,
"startedAt": "2025-12-10T02:02:54.785Z",
"imagePaths": [
{
"id": "img-1765331797511-v4ssc1hha",
@@ -170,7 +170,7 @@
"mimeType": "image/png"
}
],
"startedAt": "2025-12-10T02:02:54.785Z",
"skipTests": true,
"model": "opus",
"thinkingLevel": "none"
},
@@ -180,7 +180,7 @@
"description": "Add claude and codex to the left sidebar of settings so its will scroll to thoes sections as well",
"steps": [],
"status": "verified",
"skipTests": true,
"startedAt": "2025-12-10T09:32:31.638Z",
"imagePaths": [
{
"id": "img-1765358823366-6vchdhwsj",
@@ -189,7 +189,7 @@
"mimeType": "image/png"
}
],
"startedAt": "2025-12-10T09:32:31.638Z",
"skipTests": true,
"model": "sonnet",
"thinkingLevel": "none"
},
@@ -199,9 +199,9 @@
"description": "When u write new feature for ai agent and attacht context images and change tab to choose diff model and go back to prompt tab the image preview break and im not sure if it even saved properly in state to be later attached check it out for me",
"steps": [],
"status": "verified",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-10T09:59:02.988Z",
"imagePaths": [],
"skipTests": true,
"summary": "Fixed image preview breaking when switching tabs in Add Feature modal. Added previewMap/onPreviewMapChange props to DescriptionImageDropZone component to lift preview state up to parent. Modified: description-image-dropzone.tsx (added parent-controlled state support), board-view.tsx (added newFeaturePreviewMap and followUpPreviewMap state, wired up to DescriptionImageDropZone). Image paths were already stored correctly in state - only the preview thumbnails (base64) were lost on tab switch due to component unmounting.",
"model": "opus",
"thinkingLevel": "high"
@@ -212,7 +212,7 @@
"description": "Take a look at waiting aproval column in kanban board and fix the card that render in it u can see in attached images that they text is overlaping check other columns how we have them",
"steps": [],
"status": "verified",
"skipTests": true,
"startedAt": "2025-12-10T10:46:42.494Z",
"imagePaths": [
{
"id": "img-1765363296205-e4cwlj2j8",
@@ -233,7 +233,7 @@
"mimeType": "image/png"
}
],
"startedAt": "2025-12-10T10:46:42.494Z",
"skipTests": true,
"model": "sonnet",
"thinkingLevel": "low"
},
@@ -243,9 +243,9 @@
"description": "I want to have some abbility when executing a task on project to have some type of rewing / checkpoint system so if the changes made by agent in the project dont satisfy me / break something i can click in the ui to revert them. The best way for it would be to implement github worktress so when spin up new task claude take a look at it generate new branch that fit task issue and make it as gihub worktree then we would create a a new folder in project .automaker/worktree with branch name and clone of repo so agent can freely work one something like that ",
"steps": [],
"status": "verified",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-10T11:11:06.115Z",
"imagePaths": [],
"skipTests": true,
"summary": "Implemented Git Worktree Checkpoint/Revert System. Created: worktree-manager.js service. Modified: auto-mode-service.js (worktree integration, revert/merge methods), feature-loader.js (worktree tracking), main.js (IPC handlers), preload.js (API exposure), app-store.ts (Feature type), electron.d.ts (types), electron.ts (mock API), kanban-card.tsx (branch badge, revert/merge buttons), board-view.tsx (handlers). Features: isolated git branches per feature, branch badge on cards, revert changes button, merge to main button, file diff APIs.",
"model": "opus",
"thinkingLevel": "ultrathink"
@@ -256,24 +256,11 @@
"description": "When a agent is workig on task or when its in waiting approval column its would be nice to have some type of git diff panel and see what files got changed as well as reusing our custom themes we have in settings for the editor view of it take a look at codebase and create implementation for it",
"steps": [],
"status": "verified",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-10T11:16:54.069Z",
"imagePaths": [],
"skipTests": true,
"summary": "Added git diff panel for in-progress and waiting approval features. Created GitDiffPanel component with themed syntax highlighting. Modified: git-diff-panel.tsx (new), agent-output-modal.tsx, worktree-manager.js, auto-mode-service.js, main.js, preload.js, electron.d.ts. The panel shows changed files with +/- stats and expandable unified diff view using CSS theme variables.",
"model": "opus",
"thinkingLevel": "ultrathink"
},
{
"id": "feature-1765366278888-fobz39cc4",
"category": "Core",
"description": "Implement profile view and in the sidebar the profile view would allow user to defined different ai provider profiels like heavy-task would be claude opus model with ultrathink or debugging would be codex max. This will give user flexibillity in our model tab to quickly use own defined profiles preset of models.",
"steps": [],
"status": "waiting_approval",
"skipTests": true,
"imagePaths": [],
"startedAt": "2025-12-10T11:31:20.842Z",
"summary": "Implemented AI Profiles feature for managing model configuration presets. Created: profiles-view.tsx. Modified: app-store.ts (added AIProfile type, state, and CRUD actions), sidebar.tsx (added profiles nav item), page.tsx (added profiles view routing), board-view.tsx (added Quick Select Profile section in Add/Edit Feature dialogs). Features: 5 built-in profiles (Heavy Task, Balanced, Quick Edit, Codex Power, Codex Fast), custom profile CRUD, drag-and-drop reordering, quick profile selection in feature dialogs.",
"model": "opus",
"thinkingLevel": "high"
}
]

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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;