Merge main into feat/extend-models-support

Resolved conflicts:
- feature_list.json: Merged all features from both branches
- feature-loader.js: Included both model/thinkingLevel and error fields
- board-view.tsx: Merged model/thinkingLevel and error fields, kept currentProject check
- settings-view.tsx: Merged CLI status checks with navigation/scroll code
- app-store.ts: Included both model/thinkingLevel and error fields in Feature interface

Fixed linting errors in settings-view.tsx
This commit is contained in:
Kacper
2025-12-10 10:25:13 +01:00
19 changed files with 856 additions and 395 deletions

View File

@@ -3,7 +3,6 @@
import { useState, useMemo, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { useAppStore } from "@/store/app-store";
import Link from "next/link";
import {
FolderOpen,
Plus,
@@ -11,25 +10,32 @@ import {
FileText,
LayoutGrid,
Bot,
ChevronLeft,
ChevronRight,
Folder,
X,
Wrench,
PanelLeft,
PanelLeftClose,
Sparkles,
ChevronDown,
Check,
BookOpen,
GripVertical,
Trash2,
Undo2,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
useKeyboardShortcuts,
NAV_SHORTCUTS,
@@ -37,7 +43,7 @@ import {
ACTION_SHORTCUTS,
KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts";
import { getElectronAPI, Project } from "@/lib/electron";
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
import { initializeProject } from "@/lib/project-init";
import { toast } from "sonner";
import {
@@ -145,6 +151,7 @@ function SortableProjectItem({
export function Sidebar() {
const {
projects,
trashedProjects,
currentProject,
currentView,
sidebarOpen,
@@ -152,12 +159,17 @@ export function Sidebar() {
setCurrentProject,
setCurrentView,
toggleSidebar,
removeProject,
restoreTrashedProject,
deleteTrashedProject,
emptyTrash,
reorderProjects,
} = useAppStore();
// State for project picker dropdown
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
const [showTrashDialog, setShowTrashDialog] = useState(false);
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
// Sensors for drag-and-drop
const sensors = useSensors(
@@ -239,6 +251,73 @@ export function Sidebar() {
}
}, [addProject, setCurrentProject]);
const handleRestoreProject = useCallback(
(projectId: string) => {
restoreTrashedProject(projectId);
toast.success("Project restored", {
description: "Added back to your project list.",
});
setShowTrashDialog(false);
},
[restoreTrashedProject]
);
const handleDeleteProjectFromDisk = useCallback(
async (trashedProject: TrashedProject) => {
const confirmed = window.confirm(
`Delete "${trashedProject.name}" from disk?\nThis sends the folder to your system Trash.`
);
if (!confirmed) return;
setActiveTrashId(trashedProject.id);
try {
const api = getElectronAPI();
if (!api.trashItem) {
throw new Error("System Trash is not available in this build.");
}
const result = await api.trashItem(trashedProject.path);
if (!result.success) {
throw new Error(result.error || "Failed to delete project folder");
}
deleteTrashedProject(trashedProject.id);
toast.success("Project folder sent to system Trash", {
description: trashedProject.path,
});
} catch (error) {
console.error("[Sidebar] Failed to delete project from disk:", error);
toast.error("Failed to delete project folder", {
description: error instanceof Error ? error.message : "Unknown error",
});
} finally {
setActiveTrashId(null);
}
},
[deleteTrashedProject]
);
const handleEmptyTrash = useCallback(() => {
if (trashedProjects.length === 0) {
setShowTrashDialog(false);
return;
}
const confirmed = window.confirm(
"Clear all trashed projects from Automaker? This does not delete folders from disk."
);
if (!confirmed) return;
setIsEmptyingTrash(true);
try {
emptyTrash();
toast.success("Trash cleared");
setShowTrashDialog(false);
} finally {
setIsEmptyingTrash(false);
}
}, [emptyTrash, trashedProjects.length]);
const navSections: NavSection[] = [
{
label: "Project",
@@ -428,16 +507,16 @@ export function Sidebar() {
onClick={() => setCurrentView("welcome")}
data-testid="logo-button"
>
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg group">
<div className="relative flex items-center justify-center rounded-lg group">
<img
src="/icon_gold.png"
src="/logo.png"
alt="Automaker Logo"
className="w-8 h-8 group-hover:rotate-12 transition-transform"
className="size-8 group-hover:rotate-12 transition-transform"
/>
</div>
<span
className={cn(
"ml-3 font-bold text-sidebar-foreground text-base tracking-tight",
"ml-1 font-bold text-sidebar-foreground text-base tracking-tight",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
@@ -455,7 +534,7 @@ export function Sidebar() {
title="New Project"
data-testid="new-project-button"
>
<Plus className="w-4 h-4 flex-shrink-0" />
<Plus className="w-4 h-4 shrink-0" />
<span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap">
New
</span>
@@ -467,13 +546,23 @@ export function Sidebar() {
data-testid="open-project-button"
>
<FolderOpen className="w-4 h-4 shrink-0" />
<span className="ml-2 text-sm font-medium hidden lg:block whitespace-nowrap">
Open
</span>
<span className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-white/5 border border-white/10 text-zinc-500 ml-2">
{ACTION_SHORTCUTS.openProject}
</span>
</button>
<button
onClick={() => setShowTrashDialog(true)}
className="group flex items-center justify-center px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-primary hover:bg-destructive/10 border border-sidebar-border"
title="Trash"
data-testid="trash-button"
>
<Trash2 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}
</span>
)}
</button>
</div>
)}
@@ -581,7 +670,7 @@ export function Sidebar() {
isActive
? "bg-sidebar-accent/50 text-foreground border border-sidebar-border"
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50",
!sidebarOpen && "justify-center"
sidebarOpen ? "justify-start" : "justify-center"
)}
title={!sidebarOpen ? item.label : undefined}
data-testid={`nav-${item.id}`}
@@ -599,7 +688,7 @@ export function Sidebar() {
/>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1",
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
@@ -665,7 +754,7 @@ export function Sidebar() {
/>
<span
className={cn(
"ml-2.5 font-medium text-sm flex-1",
"ml-2.5 font-medium text-sm flex-1 text-left",
sidebarOpen ? "hidden lg:block" : "hidden"
)}
>
@@ -691,6 +780,91 @@ export function Sidebar() {
</button>
</div>
</div>
<Dialog open={showTrashDialog} onOpenChange={setShowTrashDialog}>
<DialogContent className="bg-popover border-border max-w-2xl">
<DialogHeader>
<DialogTitle>Trash</DialogTitle>
<DialogDescription className="text-muted-foreground">
Restore projects to the sidebar or delete their folders using your
system Trash.
</DialogDescription>
</DialogHeader>
{trashedProjects.length === 0 ? (
<p className="text-sm text-muted-foreground">Trash is empty.</p>
) : (
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
{trashedProjects.map((project) => (
<div
key={project.id}
className="flex items-start justify-between gap-3 rounded-md border border-sidebar-border bg-sidebar-accent/20 p-3"
>
<div className="space-y-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{project.name}
</p>
<p className="text-xs text-muted-foreground break-all">
{project.path}
</p>
<p className="text-[11px] text-muted-foreground/80">
Trashed {new Date(project.trashedAt).toLocaleString()}
</p>
</div>
<div className="flex flex-col gap-2 shrink-0">
<Button
size="sm"
variant="secondary"
onClick={() => handleRestoreProject(project.id)}
data-testid={`restore-project-${project.id}`}
>
<Undo2 className="h-3.5 w-3.5 mr-1.5" />
Restore
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDeleteProjectFromDisk(project)}
disabled={activeTrashId === project.id}
data-testid={`delete-project-disk-${project.id}`}
>
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
{activeTrashId === project.id
? "Deleting..."
: "Delete from disk"}
</Button>
<Button
size="sm"
variant="ghost"
className="text-muted-foreground hover:text-foreground"
onClick={() => deleteTrashedProject(project.id)}
data-testid={`remove-project-${project.id}`}
>
<X className="h-3.5 w-3.5 mr-1.5" />
Remove from list
</Button>
</div>
</div>
))}
</div>
)}
<DialogFooter className="flex justify-between">
<Button variant="ghost" onClick={() => setShowTrashDialog(false)}>
Close
</Button>
{trashedProjects.length > 0 && (
<Button
variant="outline"
onClick={handleEmptyTrash}
disabled={isEmptyingTrash}
data-testid="empty-trash"
>
{isEmptyingTrash ? "Clearing..." : "Empty Trash"}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
</aside>
);
}

View File

@@ -237,7 +237,7 @@ export function AgentOutputModal({
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent
className="w-[90vw] max-w-[90vw] max-h-[80vh] flex flex-col"
className="w-[60vw] max-w-[60vw] max-h-[80vh] flex flex-col"
data-testid="agent-output-modal"
>
<DialogHeader className="flex-shrink-0">

View File

@@ -416,17 +416,35 @@ export function BoardView() {
}
}, [showAddDialog, defaultSkipTests]);
// Listen for auto mode feature completion and reload features
// Listen for auto mode feature completion and errors to reload features
useEffect(() => {
const api = getElectronAPI();
if (!api?.autoMode) return;
const { removeRunningTask } = useAppStore.getState();
const unsubscribe = api.autoMode.onEvent((event) => {
if (event.type === "auto_mode_feature_complete") {
// Reload features when a feature is completed
console.log("[Board] Feature completed, reloading features...");
loadFeatures();
} else if (event.type === "auto_mode_error") {
// Reload features when an error occurs (feature moved to waiting_approval)
console.log(
"[Board] Feature error, reloading features...",
event.error
);
// Remove from running tasks so it moves to the correct column
if (event.featureId) {
removeRunningTask(event.featureId);
}
loadFeatures();
// Show error toast
toast.error("Agent encountered an error", {
description: event.error || "Check the logs for details",
});
}
});
@@ -478,7 +496,10 @@ export function BoardView() {
const checkAllContexts = async () => {
// Check context for in_progress, waiting_approval, and verified features
const featuresWithPotentialContext = features.filter(
(f) => f.status === "in_progress" || f.status === "waiting_approval" || f.status === "verified"
(f) =>
f.status === "in_progress" ||
f.status === "waiting_approval" ||
f.status === "verified"
);
const contextChecks = await Promise.all(
featuresWithPotentialContext.map(async (f) => ({
@@ -520,6 +541,7 @@ export function BoardView() {
summary: f.summary,
model: f.model,
thinkingLevel: f.thinkingLevel,
error: f.error,
}));
await api.writeFile(
`${currentProject.path}/.automaker/feature_list.json`,
@@ -754,7 +776,9 @@ export function BoardView() {
console.log(`[Board] Deleted agent context for feature ${featureId}`);
} catch (error) {
// Context file might not exist, which is fine
console.log(`[Board] Context file not found or already deleted for feature ${featureId}`);
console.log(
`[Board] Context file not found or already deleted for feature ${featureId}`
);
}
}
@@ -767,11 +791,17 @@ export function BoardView() {
await api.deleteFile(imagePathObj.path);
console.log(`[Board] Deleted image: ${imagePathObj.path}`);
} catch (error) {
console.error(`[Board] Failed to delete image ${imagePathObj.path}:`, error);
console.error(
`[Board] Failed to delete image ${imagePathObj.path}:`,
error
);
}
}
} catch (error) {
console.error(`[Board] Error deleting images for feature ${featureId}:`, error);
console.error(
`[Board] Error deleting images for feature ${featureId}:`,
error
);
}
}
@@ -2009,10 +2039,15 @@ export function BoardView() {
try {
const contextPath = `${currentProject.path}/.automaker/agents-context/${feature.id}.md`;
await api.deleteFile(contextPath);
console.log(`[Board] Deleted agent context for feature ${feature.id}`);
console.log(
`[Board] Deleted agent context for feature ${feature.id}`
);
} catch (error) {
// Context file might not exist, which is fine
console.debug("[Board] No context file to delete for feature:", feature.id);
console.debug(
"[Board] No context file to delete for feature:",
feature.id
);
}
// Remove the feature

View File

@@ -39,7 +39,6 @@ import {
RotateCcw,
StopCircle,
Hand,
ArrowLeft,
MessageSquare,
GitCommit,
Cpu,
@@ -49,6 +48,7 @@ import {
Expand,
FileText,
MoreVertical,
AlertCircle,
} from "lucide-react";
import { CountUpTimer } from "@/components/ui/count-up-timer";
import { getElectronAPI } from "@/lib/electron";
@@ -199,7 +199,10 @@ export function KanbanCard({
"cursor-grab active:cursor-grabbing transition-all backdrop-blur-sm border-border relative kanban-card-content",
isDragging && "opacity-50 scale-105 shadow-lg",
isCurrentAutoTask &&
"border-running-indicator border-2 shadow-running-indicator/50 shadow-lg animate-pulse"
"border-running-indicator border-2 shadow-running-indicator/50 shadow-lg animate-pulse",
feature.error &&
!isCurrentAutoTask &&
"border-red-500 border-2 shadow-red-500/30 shadow-lg"
)}
data-testid={`kanban-card-${feature.id}`}
{...attributes}
@@ -214,7 +217,7 @@ export function KanbanCard({
</div>
)}
{/* Skip Tests indicator badge */}
{feature.skipTests && (
{feature.skipTests && !feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
@@ -228,6 +231,21 @@ export function KanbanCard({
<span>Manual</span>
</div>
)}
{/* Error indicator badge */}
{feature.error && (
<div
className={cn(
"absolute px-1.5 py-0.5 text-[10px] font-medium rounded flex items-center gap-1 z-10",
shortcutKey ? "top-2 left-10" : "top-2 left-2",
"bg-red-500/20 border border-red-500/50 text-red-400"
)}
data-testid={`error-badge-${feature.id}`}
title={feature.error}
>
<AlertCircle className="w-3 h-3" />
<span>Errored</span>
</div>
)}
<CardHeader className="p-3 pb-2">
{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">
@@ -255,6 +273,28 @@ export function KanbanCard({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-2" />
Edit
</DropdownMenuItem>
{onViewOutput && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
data-testid={`view-logs-${feature.id}`}
>
<FileText className="w-3 h-3 mr-2" />
Logs
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
@@ -565,55 +605,10 @@ export function KanbanCard({
Logs
</Button>
)}
{/* Move back button for skipTests verified features */}
{feature.skipTests && onMoveBackToInProgress && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-yellow-500 hover:text-yellow-500 hover:bg-yellow-500/10"
onClick={(e) => {
e.stopPropagation();
onMoveBackToInProgress();
}}
data-testid={`move-back-${feature.id}`}
>
<ArrowLeft className="w-3 h-3 mr-1" />
Back
</Button>
)}
<Button
variant="ghost"
size="sm"
className="flex-1 h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-1" />
Edit
</Button>
</>
)}
{!isCurrentAutoTask && feature.status === "waiting_approval" && (
<>
{/* Logs button if context exists */}
{hasContext && onViewOutput && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onViewOutput();
}}
data-testid={`view-output-waiting-${feature.id}`}
>
<FileText className="w-3 h-3 mr-1" />
Logs
</Button>
)}
{/* Follow-up prompt button */}
{onFollowUp && (
<Button
@@ -665,19 +660,6 @@ export function KanbanCard({
Logs
</Button>
)}
<Button
variant="ghost"
size="sm"
className="flex-1 h-7 text-xs"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
data-testid={`edit-feature-${feature.id}`}
>
<Edit className="w-3 h-3 mr-1" />
Edit
</Button>
</>
)}
</div>

View File

@@ -1,10 +1,11 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import {
Settings,
Key,
@@ -32,9 +33,28 @@ import {
Square,
Maximize2,
FlaskConical,
Trash2,
Folder,
} from "lucide-react";
import { getElectronAPI } from "@/lib/electron";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
// Navigation items for the side panel
const NAV_ITEMS = [
{ id: "api-keys", label: "API Keys", icon: Key },
{ id: "appearance", label: "Appearance", icon: Palette },
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
{ id: "danger", label: "Danger Zone", icon: Trash2 },
];
export function SettingsView() {
const {
@@ -47,6 +67,8 @@ export function SettingsView() {
setKanbanCardDetailLevel,
defaultSkipTests,
setDefaultSkipTests,
currentProject,
moveProjectToTrash,
} = useAppStore();
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
const [googleKey, setGoogleKey] = useState(apiKeys.google);
@@ -101,6 +123,9 @@ export function SettingsView() {
success: boolean;
message: string;
} | null>(null);
const [activeSection, setActiveSection] = useState("api-keys");
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setAnthropicKey(apiKeys.anthropic);
@@ -131,6 +156,52 @@ export function SettingsView() {
checkCliStatus();
}, []);
// Track scroll position to highlight active nav item
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
const handleScroll = () => {
const sections = NAV_ITEMS.map((item) => ({
id: item.id,
element: document.getElementById(item.id),
})).filter((s) => s.element);
const containerRect = container.getBoundingClientRect();
const scrollTop = container.scrollTop;
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
if (section.element) {
const rect = section.element.getBoundingClientRect();
const relativeTop = rect.top - containerRect.top + scrollTop;
if (scrollTop >= relativeTop - 100) {
setActiveSection(section.id);
break;
}
}
}
};
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, []);
const scrollToSection = useCallback((sectionId: string) => {
const element = document.getElementById(sectionId);
if (element && scrollContainerRef.current) {
const container = scrollContainerRef.current;
const containerRect = container.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const relativeTop = elementRect.top - containerRect.top + container.scrollTop;
container.scrollTo({
top: relativeTop - 24,
behavior: "smooth",
});
}
}, []);
const handleTestConnection = async () => {
setTestingConnection(true);
setTestResult(null);
@@ -157,7 +228,7 @@ export function SettingsView() {
message: data.error || "Failed to connect to Claude API.",
});
}
} catch (error) {
} catch {
setTestResult({
success: false,
message: "Network error. Please check your connection.",
@@ -193,7 +264,7 @@ export function SettingsView() {
message: data.error || "Failed to connect to Gemini API.",
});
}
} catch (error) {
} catch {
setGeminiTestResult({
success: false,
message: "Network error. Please check your connection.",
@@ -246,7 +317,7 @@ export function SettingsView() {
});
}
}
} catch (error) {
} catch {
setOpenaiTestResult({
success: false,
message: "Network error. Please check your connection.",
@@ -288,23 +359,60 @@ export function SettingsView() {
</div>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* API Keys Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Key className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
API Keys
</h2>
{/* Content Area with Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Sticky Side Navigation */}
<nav className="hidden lg:block w-48 shrink-0 border-r border-border bg-card/50 backdrop-blur-sm">
<div className="sticky top-0 p-4 space-y-1">
{NAV_ITEMS.filter((item) => item.id !== "danger" || currentProject).map(
(item) => {
const Icon = item.icon;
const isActive = activeSection === item.id;
return (
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className={cn(
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-left",
isActive
? "bg-brand-500/10 text-brand-500 border border-brand-500/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
>
<Icon
className={cn(
"w-4 h-4 shrink-0",
isActive ? "text-brand-500" : ""
)}
/>
<span className="truncate">{item.label}</span>
</button>
);
}
)}
</div>
</nav>
{/* Scrollable Content */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* API Keys Section */}
<div
id="api-keys"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Key className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
API Keys
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure your AI provider API keys. Keys are stored locally in
your browser.
</p>
</div>
<p className="text-sm text-muted-foreground">
Configure your AI provider API keys. Keys are stored locally in
your browser.
</p>
</div>
<div className="p-6 space-y-6">
{/* Claude/Anthropic API Key */}
<div className="space-y-3">
@@ -592,7 +700,7 @@ export function SettingsView() {
<div className="text-sm">
<p className="font-medium text-yellow-500">Security Notice</p>
<p className="text-yellow-500/80 text-xs mt-1">
API keys are stored in your browser's local storage. Never
API keys are stored in your browser&apos;s local storage. Never
share your API keys or commit them to version control.
</p>
</div>
@@ -780,7 +888,10 @@ export function SettingsView() {
)}
{/* Appearance Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div
id="appearance"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<Palette className="w-5 h-5 text-brand-500" />
@@ -958,7 +1069,10 @@ export function SettingsView() {
</div>
{/* Kanban Card Display Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div
id="kanban"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<LayoutGrid className="w-5 h-5 text-brand-500" />
@@ -1036,7 +1150,10 @@ export function SettingsView() {
</div>
{/* Feature Defaults Section */}
<div className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden">
<div
id="defaults"
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-border">
<div className="flex items-center gap-2 mb-2">
<FlaskConical className="w-5 h-5 text-brand-500" />
@@ -1078,6 +1195,51 @@ export function SettingsView() {
</div>
</div>
{/* Delete Project Section - Only show when a project is selected */}
{currentProject && (
<div
id="danger"
className="rounded-xl border border-destructive/30 bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-destructive/30">
<div className="flex items-center gap-2 mb-2">
<Trash2 className="w-5 h-5 text-destructive" />
<h2 className="text-lg font-semibold text-foreground">
Danger Zone
</h2>
</div>
<p className="text-sm text-muted-foreground">
Permanently remove this project from Automaker.
</p>
</div>
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{currentProject.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{currentProject.path}
</p>
</div>
</div>
<Button
variant="destructive"
onClick={() => setShowDeleteDialog(true)}
data-testid="delete-project-button"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete Project
</Button>
</div>
</div>
</div>
)}
{/* Save Button */}
<div className="flex items-center gap-4">
<Button
@@ -1105,6 +1267,64 @@ export function SettingsView() {
</div>
</div>
</div>
</div>
{/* Delete Project Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent className="bg-popover border-border max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="w-5 h-5 text-destructive" />
Delete Project
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Are you sure you want to move this project to Trash?
</DialogDescription>
</DialogHeader>
{currentProject && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{currentProject.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{currentProject.path}
</p>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
The folder will remain on disk until you permanently delete it from Trash.
</p>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="ghost"
onClick={() => setShowDeleteDialog(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => {
if (currentProject) {
moveProjectToTrash(currentProject.id);
setShowDeleteDialog(false);
}
}}
data-testid="confirm-delete-project"
>
<Trash2 className="w-4 h-4 mr-2" />
Move to Trash
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -284,11 +284,7 @@ export function WelcomeView() {
<div className="px-8 py-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl flex items-center justify-center">
<img
src="/icon_gold.png"
alt="Automaker Logo"
className="w-10 h-10"
/>
<img src="/logo.png" alt="Automaker Logo" className="w-10 h-10" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">