various fixes

This commit is contained in:
Cody Seibert
2025-12-12 20:51:01 -05:00
parent 5544031164
commit ca4809ca06
13 changed files with 496 additions and 126 deletions

View File

@@ -42,6 +42,7 @@ import {
Search,
Bug,
Activity,
Recycle,
} from "lucide-react";
import {
DropdownMenu,
@@ -70,7 +71,7 @@ import {
useKeyboardShortcutsConfig,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
import { getElectronAPI, Project, TrashedProject, RunningAgent } from "@/lib/electron";
import {
initializeProject,
hasAppSpec,
@@ -80,6 +81,7 @@ import { toast } from "sonner";
import { Sparkles, Loader2 } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import type { SpecRegenerationEvent } from "@/types/electron";
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
import {
DndContext,
DragEndEvent,
@@ -212,6 +214,7 @@ export function Sidebar() {
setProjectTheme,
setTheme,
theme: globalTheme,
moveProjectToTrash,
} = useAppStore();
// Get customizable keyboard shortcuts
@@ -225,6 +228,12 @@ export function Sidebar() {
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
// State for delete project confirmation dialog
const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false);
// State for running agents count
const [runningAgentsCount, setRunningAgentsCount] = useState(0);
// State for new project setup dialog
const [showSetupDialog, setShowSetupDialog] = useState(false);
const [setupProjectPath, setSetupProjectPath] = useState("");
@@ -334,6 +343,64 @@ export function Sidebar() {
};
}, [setCurrentView]);
// Fetch running agents count and update every 2 seconds
useEffect(() => {
const fetchRunningAgentsCount = async () => {
try {
const api = getElectronAPI();
if (api.runningAgents) {
const result = await api.runningAgents.getAll();
if (result.success && result.runningAgents) {
setRunningAgentsCount(result.runningAgents.length);
}
}
} catch (error) {
console.error("[Sidebar] Error fetching running agents count:", error);
}
};
// Initial fetch
fetchRunningAgentsCount();
// Set up interval to refresh every 2 seconds
const interval = setInterval(fetchRunningAgentsCount, 2000);
return () => clearInterval(interval);
}, []);
// Subscribe to auto-mode events to update running agents count in real-time
useEffect(() => {
const api = getElectronAPI();
if (!api.autoMode) return;
const unsubscribe = api.autoMode.onEvent((event) => {
// When a feature starts, completes, or errors, refresh the count
if (
event.type === "auto_mode_feature_complete" ||
event.type === "auto_mode_error" ||
event.type === "auto_mode_feature_started"
) {
const fetchRunningAgentsCount = async () => {
try {
if (api.runningAgents) {
const result = await api.runningAgents.getAll();
if (result.success && result.runningAgents) {
setRunningAgentsCount(result.runningAgents.length);
}
}
} catch (error) {
console.error("[Sidebar] Error fetching running agents count:", error);
}
};
fetchRunningAgentsCount();
}
});
return () => {
unsubscribe();
};
}, []);
// Handle creating initial spec for new project
const handleCreateInitialSpec = useCallback(async () => {
if (!setupProjectPath || !projectOverview.trim()) return;
@@ -534,14 +601,14 @@ export function Sidebar() {
}
const confirmed = window.confirm(
"Clear all trashed projects from Automaker? This does not delete folders from disk."
"Clear all projects from recycle bin? This does not delete folders from disk."
);
if (!confirmed) return;
setIsEmptyingTrash(true);
try {
emptyTrash();
toast.success("Trash cleared");
toast.success("Recycle bin cleared");
setShowTrashDialog(false);
} finally {
setIsEmptyingTrash(false);
@@ -830,10 +897,10 @@ export function Sidebar() {
<button
onClick={() => setShowTrashDialog(true)}
className="group flex items-center justify-center px-3 h-[42px] rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-primary hover:bg-destructive/10 border border-sidebar-border"
title="Trash"
title="Recycle Bin"
data-testid="trash-button"
>
<Trash2 className="size-4 shrink-0" />
<Recycle className="size-4 shrink-0" />
{trashedProjects.length > 0 && (
<span className="absolute -top-[2px] -right-[2px] flex items-center justify-center w-5 h-5 text-[10px] font-medium rounded-full text-brand-500">
{trashedProjects.length > 9 ? "9+" : trashedProjects.length}
@@ -1039,6 +1106,17 @@ export function Sidebar() {
</DropdownMenuItem>
</>
)}
{/* Move to Trash Section */}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setShowDeleteProjectDialog(true)}
className="text-destructive focus:text-destructive focus:bg-destructive/10"
data-testid="move-project-to-trash"
>
<Trash2 className="w-4 h-4 mr-2" />
<span>Move to Trash</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
@@ -1242,7 +1320,7 @@ export function Sidebar() {
<Dialog open={showTrashDialog} onOpenChange={setShowTrashDialog}>
<DialogContent className="bg-popover border-border max-w-2xl">
<DialogHeader>
<DialogTitle>Trash</DialogTitle>
<DialogTitle>Recycle Bin</DialogTitle>
<DialogDescription className="text-muted-foreground">
Restore projects to the sidebar or delete their folders using your
system Trash.
@@ -1250,7 +1328,7 @@ export function Sidebar() {
</DialogHeader>
{trashedProjects.length === 0 ? (
<p className="text-sm text-muted-foreground">Trash is empty.</p>
<p className="text-sm text-muted-foreground">Recycle bin is empty.</p>
) : (
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
{trashedProjects.map((project) => (
@@ -1318,7 +1396,7 @@ export function Sidebar() {
disabled={isEmptyingTrash}
data-testid="empty-trash"
>
{isEmptyingTrash ? "Clearing..." : "Empty Trash"}
{isEmptyingTrash ? "Clearing..." : "Empty Recycle Bin"}
</Button>
)}
</DialogFooter>
@@ -1421,6 +1499,14 @@ export function Sidebar() {
</button>
</div>
)}
{/* Delete Project Confirmation Dialog */}
<DeleteProjectDialog
open={showDeleteProjectDialog}
onOpenChange={setShowDeleteProjectDialog}
project={currentProject}
onConfirm={moveProjectToTrash}
/>
</aside>
);
}

View File

@@ -17,6 +17,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import {
FolderPlus,
FolderOpen,
Rocket,
ExternalLink,
Check,
@@ -28,6 +29,7 @@ import { starterTemplates, type StarterTemplate } from "@/lib/templates";
import { getElectronAPI } from "@/lib/electron";
import { getHttpApiClient } from "@/lib/http-api-client";
import { cn } from "@/lib/utils";
import { useFileBrowser } from "@/contexts/file-browser-context";
interface ValidationErrors {
projectName?: boolean;
@@ -69,6 +71,7 @@ export function NewProjectModal({
const [useCustomUrl, setUseCustomUrl] = useState(false);
const [customUrl, setCustomUrl] = useState("");
const [errors, setErrors] = useState<ValidationErrors>({});
const { openFileBrowser } = useFileBrowser();
// Fetch workspace directory when modal opens
useEffect(() => {
@@ -181,6 +184,20 @@ export function NewProjectModal({
}
};
const handleBrowseDirectory = async () => {
const selectedPath = await openFileBrowser({
title: "Select Base Project Directory",
description: "Choose the parent directory where your project will be created",
});
if (selectedPath) {
setWorkspaceDir(selectedPath);
// Clear any workspace error when a valid directory is selected
if (errors.workspaceDir) {
setErrors((prev) => ({ ...prev, workspaceDir: false }));
}
}
};
const projectPath = workspaceDir && projectName ? `${workspaceDir}/${projectName}` : "";
return (
@@ -226,16 +243,28 @@ export function NewProjectModal({
"flex items-center gap-2 text-sm",
errors.workspaceDir ? "text-red-500" : "text-muted-foreground"
)}>
<Folder className="w-4 h-4" />
<span>
<Folder className="w-4 h-4 shrink-0" />
<span className="flex-1 min-w-0">
{isLoadingWorkspace ? (
"Loading workspace..."
) : workspaceDir ? (
<>Will be created at: <code className="text-xs bg-muted px-1.5 py-0.5 rounded">{projectPath || "..."}</code></>
<>Will be created at: <code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">{projectPath || "..."}</code></>
) : (
<span className="text-red-500">No workspace configured - please configure WORKSPACE_DIR</span>
<span className="text-red-500">No workspace configured</span>
)}
</span>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleBrowseDirectory}
disabled={isLoadingWorkspace}
className="shrink-0 h-7 px-2 text-xs"
data-testid="browse-directory-button"
>
<FolderOpen className="w-3.5 h-3.5 mr-1" />
Browse
</Button>
</div>
</div>

View File

@@ -867,7 +867,8 @@ export function BoardView() {
// features often have skipTests=true, and we want status-based handling first
if (targetStatus === "verified") {
moveFeature(featureId, "verified");
persistFeatureUpdate(featureId, { status: "verified" });
// Clear justFinished flag when manually verifying via drag
persistFeatureUpdate(featureId, { status: "verified", justFinished: false });
toast.success("Feature verified", {
description: `Manually verified: ${draggedFeature.description.slice(
0,
@@ -877,7 +878,8 @@ export function BoardView() {
} else if (targetStatus === "backlog") {
// Allow moving waiting_approval cards back to backlog
moveFeature(featureId, "backlog");
persistFeatureUpdate(featureId, { status: "backlog" });
// Clear justFinished flag when moving back to backlog
persistFeatureUpdate(featureId, { status: "backlog", justFinished: false });
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
@@ -1198,7 +1200,8 @@ export function BoardView() {
description: feature.description,
});
moveFeature(feature.id, "verified");
persistFeatureUpdate(feature.id, { status: "verified" });
// Clear justFinished flag when manually verifying
persistFeatureUpdate(feature.id, { status: "verified", justFinished: false });
toast.success("Feature verified", {
description: `Marked as verified: ${feature.description.slice(0, 50)}${
feature.description.length > 50 ? "..." : ""
@@ -1264,9 +1267,11 @@ export function BoardView() {
}
// Move feature back to in_progress before sending follow-up
// Clear justFinished flag since user is now interacting with it
const updates = {
status: "in_progress" as const,
startedAt: new Date().toISOString(),
justFinished: false,
};
updateFeature(featureId, updates);
persistFeatureUpdate(featureId, updates);
@@ -1525,6 +1530,14 @@ export function BoardView() {
}
});
// Sort waiting_approval column: justFinished features go to the top
map.waiting_approval.sort((a, b) => {
// Features with justFinished=true should appear first
if (a.justFinished && !b.justFinished) return -1;
if (!a.justFinished && b.justFinished) return 1;
return 0; // Keep original order for features with same justFinished status
});
return map;
}, [features, runningAutoTasks, searchQuery]);

View File

@@ -18,6 +18,7 @@ import {
import { cn } from "@/lib/utils";
import { getElectronAPI } from "@/lib/electron";
import { Markdown } from "@/components/ui/markdown";
import { useFileBrowser } from "@/contexts/file-browser-context";
interface InterviewMessage {
id: string;
@@ -65,6 +66,7 @@ const INTERVIEW_QUESTIONS = [
export function InterviewView() {
const { setCurrentView, addProject, setCurrentProject, setAppSpec } =
useAppStore();
const { openFileBrowser } = useFileBrowser();
const [input, setInput] = useState("");
const [messages, setMessages] = useState<InterviewMessage[]>([]);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
@@ -286,11 +288,13 @@ export function InterviewView() {
};
const handleSelectDirectory = async () => {
const api = getElectronAPI();
const result = await api.openDirectory();
const selectedPath = await openFileBrowser({
title: "Select Base Directory",
description: "Choose the parent directory where your new project will be created",
});
if (!result.canceled && result.filePaths[0]) {
setProjectPath(result.filePaths[0]);
if (selectedPath) {
setProjectPath(selectedPath);
}
};

View File

@@ -27,7 +27,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Feature, useAppStore } from "@/store/app-store";
import { Feature, useAppStore, ThinkingLevel } from "@/store/app-store";
import {
GripVertical,
Edit,
@@ -55,6 +55,7 @@ import {
GitMerge,
ChevronDown,
ChevronUp,
Brain,
} from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer";
import { getElectronAPI } from "@/lib/electron";
@@ -72,6 +73,21 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
/**
* Formats thinking level for compact display
*/
function formatThinkingLevel(level: ThinkingLevel | undefined): string {
if (!level || level === "none") return "";
const labels: Record<ThinkingLevel, string> = {
none: "",
low: "Low",
medium: "Med",
high: "High",
ultrathink: "Ultra",
};
return labels[level];
}
interface KanbanCardProps {
feature: Feature;
onEdit: () => void;
@@ -280,6 +296,21 @@ export const KanbanCard = memo(function KanbanCard({
<span>Errored</span>
</div>
)}
{/* Just Finished indicator badge - shows when agent just completed work */}
{feature.justFinished && feature.status === "waiting_approval" && !feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
feature.skipTests ? "top-8 left-2" : "top-2 left-2",
"bg-green-500/20 border border-green-500/50 text-green-400 animate-pulse"
)}
data-testid={`just-finished-badge-${feature.id}`}
title="Agent just finished working on this feature"
>
<Sparkles className="w-3 h-3" />
<span>Done</span>
</div>
)}
{/* Branch badge - show when feature has a worktree */}
{hasWorktree && !isCurrentAutoTask && (
<TooltipProvider delayDuration={300}>
@@ -289,8 +320,8 @@ export const KanbanCard = memo(function KanbanCard({
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10 cursor-default",
"bg-purple-500/20 border border-purple-500/50 text-purple-400",
// Position below error badge if present, otherwise use normal position
feature.error || feature.skipTests
// Position below other badges if present, otherwise use normal position
feature.error || feature.skipTests || (feature.justFinished && feature.status === "waiting_approval")
? "top-8 left-2"
: "top-2 left-2"
)}
@@ -310,14 +341,17 @@ export const KanbanCard = memo(function KanbanCard({
className={cn(
"p-3 pb-2 block", // Reset grid layout to block for custom kanban card layout
// Add extra top padding when badges are present to prevent text overlap
(feature.skipTests || feature.error) && "pt-10",
(feature.skipTests || feature.error || (feature.justFinished && feature.status === "waiting_approval")) && "pt-10",
// Add even more top padding when both badges and branch are shown
hasWorktree && (feature.skipTests || feature.error) && "pt-14"
hasWorktree && (feature.skipTests || feature.error || (feature.justFinished && feature.status === "waiting_approval")) && "pt-14"
)}
>
{isCurrentAutoTask && (
<div className="absolute top-2 right-2 flex items-center justify-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
<Loader2 className="w-4 h-4 text-running-indicator animate-spin" />
<span className="text-xs text-running-indicator font-medium">
{formatModelName(feature.model ?? DEFAULT_MODEL)}
</span>
{feature.startedAt && (
<CountUpTimer
startedAt={feature.startedAt}
@@ -452,6 +486,28 @@ export const KanbanCard = memo(function KanbanCard({
</div>
)}
{/* Model/Preset Info for Backlog Cards - Show in Detailed mode */}
{showAgentInfo && feature.status === "backlog" && (
<div className="mb-3 space-y-2 overflow-hidden">
<div className="flex items-center gap-2 text-xs flex-wrap">
<div className="flex items-center gap-1 text-cyan-400">
<Cpu className="w-3 h-3" />
<span className="font-medium">
{formatModelName(feature.model ?? DEFAULT_MODEL)}
</span>
</div>
{feature.thinkingLevel && feature.thinkingLevel !== "none" && (
<div className="flex items-center gap-1 text-purple-400">
<Brain className="w-3 h-3" />
<span className="font-medium">
{formatThinkingLevel(feature.thinkingLevel)}
</span>
</div>
)}
</div>
</div>
)}
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
{/* Detailed mode: Show all agent info */}
{showAgentInfo && feature.status !== "backlog" && agentInfo && (

View File

@@ -181,7 +181,8 @@ export function WelcomeView() {
if (!result.canceled && result.filePaths[0]) {
const path = result.filePaths[0];
// Extract folder name from path (works on both Windows and Mac/Linux)
const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
const name =
path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
await initializeAndOpenProject(path, name);
}
}
@@ -193,7 +194,8 @@ export function WelcomeView() {
if (!result.canceled && result.filePaths[0]) {
const path = result.filePaths[0];
const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
const name =
path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project";
await initializeAndOpenProject(path, name);
}
}
@@ -231,7 +233,10 @@ export function WelcomeView() {
/**
* Create a blank project with just .automaker directory structure
*/
const handleCreateBlankProject = async (projectName: string, parentDir: string) => {
const handleCreateBlankProject = async (
projectName: string,
parentDir: string
) => {
setIsCreating(true);
try {
const api = getElectronAPI();
@@ -359,11 +364,15 @@ export function WelcomeView() {
</overview>
<technology_stack>
${template.techStack.map((tech) => `<technology>${tech}</technology>`).join("\n ")}
${template.techStack
.map((tech) => `<technology>${tech}</technology>`)
.join("\n ")}
</technology_stack>
<core_capabilities>
${template.features.map((feature) => `<capability>${feature}</capability>`).join("\n ")}
${template.features
.map((feature) => `<capability>${feature}</capability>`)
.join("\n ")}
</core_capabilities>
<implemented_features>