mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge branch 'main' into feat/extend-models-support
This commit is contained in:
@@ -19,10 +19,10 @@ import {
|
||||
PanelLeft,
|
||||
PanelLeftClose,
|
||||
Sparkles,
|
||||
Cpu,
|
||||
ChevronDown,
|
||||
Check,
|
||||
BookOpen,
|
||||
GripVertical,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -37,9 +37,23 @@ import {
|
||||
ACTION_SHORTCUTS,
|
||||
KeyboardShortcut,
|
||||
} from "@/hooks/use-keyboard-shortcuts";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { getElectronAPI, Project } from "@/lib/electron";
|
||||
import { initializeProject } from "@/lib/project-init";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
interface NavSection {
|
||||
label?: string;
|
||||
@@ -53,6 +67,81 @@ interface NavItem {
|
||||
shortcut?: string;
|
||||
}
|
||||
|
||||
// Sortable Project Item Component
|
||||
interface SortableProjectItemProps {
|
||||
project: Project;
|
||||
index: number;
|
||||
currentProjectId: string | undefined;
|
||||
onSelect: (project: Project) => void;
|
||||
}
|
||||
|
||||
function SortableProjectItem({
|
||||
project,
|
||||
index,
|
||||
currentProjectId,
|
||||
onSelect,
|
||||
}: SortableProjectItemProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: project.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent",
|
||||
isDragging && "bg-accent shadow-lg"
|
||||
)}
|
||||
data-testid={`project-option-${project.id}`}
|
||||
>
|
||||
{/* Drag Handle */}
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="p-0.5 rounded hover:bg-sidebar-accent/20 cursor-grab active:cursor-grabbing"
|
||||
data-testid={`project-drag-handle-${project.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<GripVertical className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{/* Hotkey indicator */}
|
||||
{index < 9 && (
|
||||
<span
|
||||
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
|
||||
data-testid={`project-hotkey-${index + 1}`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Project content - clickable area */}
|
||||
<div
|
||||
className="flex items-center gap-2 flex-1 min-w-0"
|
||||
onClick={() => onSelect(project)}
|
||||
>
|
||||
<Folder className="h-4 w-4 shrink-0" />
|
||||
<span className="flex-1 truncate text-sm">{project.name}</span>
|
||||
{currentProjectId === project.id && (
|
||||
<Check className="h-4 w-4 text-brand-500 shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const {
|
||||
projects,
|
||||
@@ -64,11 +153,38 @@ export function Sidebar() {
|
||||
setCurrentView,
|
||||
toggleSidebar,
|
||||
removeProject,
|
||||
reorderProjects,
|
||||
} = useAppStore();
|
||||
|
||||
// State for project picker dropdown
|
||||
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
|
||||
|
||||
// Sensors for drag-and-drop
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 5, // Small distance to start drag
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Handle drag end for reordering projects
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = projects.findIndex((p) => p.id === active.id);
|
||||
const newIndex = projects.findIndex((p) => p.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
reorderProjects(oldIndex, newIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
[projects, reorderProjects]
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens the system folder selection dialog and initializes the selected project.
|
||||
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
||||
@@ -312,8 +428,12 @@ export function Sidebar() {
|
||||
onClick={() => setCurrentView("welcome")}
|
||||
data-testid="logo-button"
|
||||
>
|
||||
<div className="relative flex items-center justify-center w-8 h-8 bg-linear-to-br from-brand-500 to-brand-600 rounded-lg shadow-lg shadow-brand-500/20 group">
|
||||
<Cpu className="text-primary-foreground w-5 h-5 group-hover:rotate-12 transition-transform" />
|
||||
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg group">
|
||||
<img
|
||||
src="/icon_gold.png"
|
||||
alt="Automaker Logo"
|
||||
className="w-8 h-8 group-hover:rotate-12 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
@@ -387,35 +507,33 @@ export function Sidebar() {
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 bg-popover border-border"
|
||||
className="w-64 bg-popover border-border p-1"
|
||||
align="start"
|
||||
data-testid="project-picker-dropdown"
|
||||
>
|
||||
{projects.map((project, index) => (
|
||||
<DropdownMenuItem
|
||||
key={project.id}
|
||||
onClick={() => {
|
||||
setCurrentProject(project);
|
||||
setIsProjectPickerOpen(false);
|
||||
}}
|
||||
className="flex items-center gap-2 cursor-pointer text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
data-testid={`project-option-${project.id}`}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={projects.map((p) => p.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{index < 9 && (
|
||||
<span
|
||||
className="flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-sidebar-accent/10 border border-sidebar-border text-muted-foreground"
|
||||
data-testid={`project-hotkey-${index + 1}`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
<Folder className="h-4 w-4" />
|
||||
<span className="flex-1 truncate">{project.name}</span>
|
||||
{currentProject?.id === project.id && (
|
||||
<Check className="h-4 w-4 text-brand-500" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{projects.map((project, index) => (
|
||||
<SortableProjectItem
|
||||
key={project.id}
|
||||
project={project}
|
||||
index={index}
|
||||
currentProjectId={currentProject?.id}
|
||||
onSelect={(p) => {
|
||||
setCurrentProject(p);
|
||||
setIsProjectPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils";
|
||||
import { ImageIcon, X, Loader2 } from "lucide-react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
export interface FeatureImagePath {
|
||||
id: string;
|
||||
@@ -51,6 +52,7 @@ export function DescriptionImageDropZone({
|
||||
new Map()
|
||||
);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const currentProject = useAppStore((state) => state.currentProject);
|
||||
|
||||
const fileToBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -76,11 +78,14 @@ export function DescriptionImageDropZone({
|
||||
const api = getElectronAPI();
|
||||
// Check if saveImageToTemp method exists
|
||||
if (!api.saveImageToTemp) {
|
||||
// Fallback for mock API - return a mock path
|
||||
// Fallback for mock API - return a mock path in .automaker/images
|
||||
console.log("[DescriptionImageDropZone] Using mock path for image");
|
||||
return `/tmp/automaker-images/${Date.now()}_${filename}`;
|
||||
return `.automaker/images/${Date.now()}_${filename}`;
|
||||
}
|
||||
const result = await api.saveImageToTemp(base64Data, filename, mimeType);
|
||||
|
||||
// Get projectPath from the store if available
|
||||
const projectPath = currentProject?.path;
|
||||
const result = await api.saveImageToTemp(base64Data, filename, mimeType, projectPath);
|
||||
if (result.success && result.path) {
|
||||
return result.path;
|
||||
}
|
||||
@@ -130,7 +135,7 @@ export function DescriptionImageDropZone({
|
||||
const tempPath = await saveImageToTemp(base64, file.name, file.type);
|
||||
|
||||
if (tempPath) {
|
||||
const imageId = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const imageId = `img-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
const imagePathRef: FeatureImagePath = {
|
||||
id: imageId,
|
||||
path: tempPath,
|
||||
|
||||
@@ -144,7 +144,7 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
|
||||
{entry.title}
|
||||
</span>
|
||||
|
||||
<span className="text-sm text-zinc-400 truncate flex-1 ml-2">
|
||||
<span className="text-xs text-zinc-400 truncate flex-1 ml-2">
|
||||
{!isExpanded &&
|
||||
entry.content.slice(0, 80) +
|
||||
(entry.content.length > 80 ? "..." : "")}
|
||||
@@ -156,11 +156,11 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) {
|
||||
className="px-4 pb-3 pt-1"
|
||||
data-testid={`log-entry-content-${entry.id}`}
|
||||
>
|
||||
<div className="font-mono text-sm space-y-1">
|
||||
<div className="font-mono text-xs space-y-1">
|
||||
{formattedContent.map((part, index) => (
|
||||
<div key={index}>
|
||||
{part.type === "json" ? (
|
||||
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-purple-300">
|
||||
<pre className="bg-zinc-900/50 rounded p-2 overflow-x-auto text-xs text-primary">
|
||||
{part.content}
|
||||
</pre>
|
||||
) : (
|
||||
|
||||
@@ -237,13 +237,13 @@ export function AgentOutputModal({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className="max-w-4xl max-h-[80vh] flex flex-col"
|
||||
className="w-[90vw] max-w-[90vw] max-h-[80vh] flex flex-col"
|
||||
data-testid="agent-output-modal"
|
||||
>
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
|
||||
<Loader2 className="w-5 h-5 text-primary animate-spin" />
|
||||
Agent Output
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
||||
@@ -251,7 +251,7 @@ export function AgentOutputModal({
|
||||
onClick={() => setViewMode("parsed")}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
||||
viewMode === "parsed"
|
||||
? "bg-purple-500/20 text-purple-300 shadow-sm"
|
||||
? "bg-primary/20 text-primary shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="view-mode-parsed"
|
||||
@@ -263,7 +263,7 @@ export function AgentOutputModal({
|
||||
onClick={() => setViewMode("raw")}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
||||
viewMode === "raw"
|
||||
? "bg-purple-500/20 text-purple-300 shadow-sm"
|
||||
? "bg-primary/20 text-primary shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
}`}
|
||||
data-testid="view-mode-raw"
|
||||
@@ -284,7 +284,7 @@ export function AgentOutputModal({
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-sm min-h-[400px] max-h-[60vh]"
|
||||
className="flex-1 overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs min-h-[400px] max-h-[60vh]"
|
||||
>
|
||||
{isLoading && !output ? (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useAppStore, AutoModeActivity } from "@/store/app-store";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
Wrench,
|
||||
Play,
|
||||
X,
|
||||
ClipboardList,
|
||||
Zap,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AutoModeLogProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function AutoModeLog({ onClose }: AutoModeLogProps) {
|
||||
const { autoModeActivityLog, features, clearAutoModeActivity } =
|
||||
useAppStore();
|
||||
|
||||
const getActivityIcon = (type: AutoModeActivity["type"]) => {
|
||||
switch (type) {
|
||||
case "start":
|
||||
return <Play className="w-4 h-4 text-blue-500" />;
|
||||
case "progress":
|
||||
return <Loader2 className="w-4 h-4 text-purple-500 animate-spin" />;
|
||||
case "tool":
|
||||
return <Wrench className="w-4 h-4 text-yellow-500" />;
|
||||
case "complete":
|
||||
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
||||
case "error":
|
||||
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
||||
case "planning":
|
||||
return (
|
||||
<ClipboardList
|
||||
className="w-4 h-4 text-cyan-500"
|
||||
data-testid="planning-phase-icon"
|
||||
/>
|
||||
);
|
||||
case "action":
|
||||
return (
|
||||
<Zap
|
||||
className="w-4 h-4 text-orange-500"
|
||||
data-testid="action-phase-icon"
|
||||
/>
|
||||
);
|
||||
case "verification":
|
||||
return (
|
||||
<ShieldCheck
|
||||
className="w-4 h-4 text-emerald-500"
|
||||
data-testid="verification-phase-icon"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getActivityColor = (type: AutoModeActivity["type"]) => {
|
||||
switch (type) {
|
||||
case "start":
|
||||
return "border-l-blue-500";
|
||||
case "progress":
|
||||
return "border-l-purple-500";
|
||||
case "tool":
|
||||
return "border-l-yellow-500";
|
||||
case "complete":
|
||||
return "border-l-green-500";
|
||||
case "error":
|
||||
return "border-l-red-500";
|
||||
case "planning":
|
||||
return "border-l-cyan-500";
|
||||
case "action":
|
||||
return "border-l-orange-500";
|
||||
case "verification":
|
||||
return "border-l-emerald-500";
|
||||
}
|
||||
};
|
||||
|
||||
const getFeatureDescription = (featureId: string) => {
|
||||
const feature = features.find((f) => f.id === featureId);
|
||||
return feature?.description || "Unknown feature";
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return new Date(date).toLocaleTimeString("en-US", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col border-border bg-card backdrop-blur-sm">
|
||||
<CardHeader className="p-4 border-b border-border flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-5 h-5 text-purple-500 animate-spin" />
|
||||
<CardTitle className="text-lg">Auto Mode Activity</CardTitle>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearAutoModeActivity}
|
||||
className="h-8"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
{onClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-8"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div className="p-4 space-y-2">
|
||||
{autoModeActivityLog.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p className="text-sm">No activity yet</p>
|
||||
<p className="text-xs mt-1">
|
||||
Start auto mode to see activity here
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
autoModeActivityLog
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((activity) => (
|
||||
<div
|
||||
key={activity.id}
|
||||
className={cn(
|
||||
"p-3 rounded-lg bg-secondary border-l-4 hover:bg-accent transition-colors",
|
||||
getActivityColor(activity.type)
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">
|
||||
{getActivityIcon(activity.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline gap-2 mb-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatTime(activity.timestamp)}
|
||||
</span>
|
||||
<span className="text-xs font-medium text-blue-400 truncate">
|
||||
{getFeatureDescription(activity.featureId)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground break-words">
|
||||
{activity.message}
|
||||
</p>
|
||||
{activity.tool && (
|
||||
<div className="mt-1 flex items-center gap-1">
|
||||
<Wrench className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{activity.tool}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { KanbanColumn } from "./kanban-column";
|
||||
import { KanbanCard } from "./kanban-card";
|
||||
import { AutoModeLog } from "./auto-mode-log";
|
||||
import { AgentOutputModal } from "./agent-output-modal";
|
||||
import {
|
||||
Plus,
|
||||
@@ -61,8 +60,6 @@ import {
|
||||
Play,
|
||||
StopCircle,
|
||||
Loader2,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
Users,
|
||||
Trash2,
|
||||
FastForward,
|
||||
@@ -186,7 +183,6 @@ export function BoardView() {
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [showActivityLog, setShowActivityLog] = useState(false);
|
||||
const [showOutputModal, setShowOutputModal] = useState(false);
|
||||
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
||||
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(
|
||||
@@ -420,12 +416,6 @@ export function BoardView() {
|
||||
}
|
||||
}, [showAddDialog, defaultSkipTests]);
|
||||
|
||||
// Auto-show activity log when auto mode starts
|
||||
useEffect(() => {
|
||||
if (autoMode.isRunning && !showActivityLog) {
|
||||
setShowActivityLog(true);
|
||||
}
|
||||
}, [autoMode.isRunning, showActivityLog]);
|
||||
|
||||
// Listen for auto mode feature completion and reload features
|
||||
useEffect(() => {
|
||||
@@ -486,11 +476,12 @@ export function BoardView() {
|
||||
// Check which features have context files
|
||||
useEffect(() => {
|
||||
const checkAllContexts = async () => {
|
||||
const inProgressFeatures = features.filter(
|
||||
(f) => f.status === "in_progress"
|
||||
// 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"
|
||||
);
|
||||
const contextChecks = await Promise.all(
|
||||
inProgressFeatures.map(async (f) => ({
|
||||
featuresWithPotentialContext.map(async (f) => ({
|
||||
id: f.id,
|
||||
hasContext: await checkContextExists(f.id),
|
||||
}))
|
||||
@@ -754,6 +745,36 @@ export function BoardView() {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete agent context file if it exists
|
||||
if (currentProject) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const contextPath = `${currentProject.path}/.automaker/agents-context/${featureId}.md`;
|
||||
await api.deleteFile(contextPath);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete attached images if they exist
|
||||
if (feature.imagePaths && feature.imagePaths.length > 0) {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
for (const imagePathObj of feature.imagePaths) {
|
||||
try {
|
||||
await api.deleteFile(imagePathObj.path);
|
||||
console.log(`[Board] Deleted image: ${imagePathObj.path}`);
|
||||
} catch (error) {
|
||||
console.error(`[Board] Failed to delete image ${imagePathObj.path}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Board] Error deleting images for feature ${featureId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the feature immediately without confirmation
|
||||
removeFeature(featureId);
|
||||
};
|
||||
@@ -1308,23 +1329,6 @@ export function BoardView() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMounted && autoMode.isRunning && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowActivityLog(!showActivityLog)}
|
||||
data-testid="toggle-activity-log"
|
||||
>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin text-purple-500" />
|
||||
Activity
|
||||
{showActivityLog ? (
|
||||
<ChevronDown className="w-4 h-4 ml-2" />
|
||||
) : (
|
||||
<ChevronUp className="w-4 h-4 ml-2" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
@@ -1345,12 +1349,7 @@ export function BoardView() {
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Kanban Columns */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 overflow-x-auto p-4",
|
||||
showActivityLog && "transition-all"
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 overflow-x-auto p-4">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={collisionDetectionStrategy}
|
||||
@@ -1453,13 +1452,6 @@ export function BoardView() {
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
{/* Activity Log Panel */}
|
||||
{showActivityLog && (
|
||||
<div className="w-96 border-l border-border flex-shrink-0">
|
||||
<AutoModeLog onClose={() => setShowActivityLog(false)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Feature Dialog */}
|
||||
@@ -1995,6 +1987,8 @@ export function BoardView() {
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
const verifiedFeatures = getColumnFeatures("verified");
|
||||
const api = getElectronAPI();
|
||||
|
||||
for (const feature of verifiedFeatures) {
|
||||
// Check if the feature is currently running
|
||||
const isRunning = runningAutoTasks.includes(feature.id);
|
||||
@@ -2011,6 +2005,16 @@ export function BoardView() {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete agent context file if it exists
|
||||
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}`);
|
||||
} catch (error) {
|
||||
// Context file might not exist, which is fine
|
||||
console.debug("[Board] No context file to delete for feature:", feature.id);
|
||||
}
|
||||
|
||||
// Remove the feature
|
||||
removeFeature(feature.id);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Feature, useAppStore } from "@/store/app-store";
|
||||
import {
|
||||
GripVertical,
|
||||
@@ -32,7 +38,7 @@ import {
|
||||
PlayCircle,
|
||||
RotateCcw,
|
||||
StopCircle,
|
||||
FlaskConical,
|
||||
Hand,
|
||||
ArrowLeft,
|
||||
MessageSquare,
|
||||
GitCommit,
|
||||
@@ -41,6 +47,8 @@ import {
|
||||
ListTodo,
|
||||
Sparkles,
|
||||
Expand,
|
||||
FileText,
|
||||
MoreVertical,
|
||||
} from "lucide-react";
|
||||
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
@@ -101,9 +109,6 @@ export function KanbanCard({
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
||||
const showProgressBar =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
|
||||
// Load context file for in_progress, waiting_approval, and verified features
|
||||
useEffect(() => {
|
||||
@@ -219,17 +224,14 @@ export function KanbanCard({
|
||||
data-testid={`skip-tests-badge-${feature.id}`}
|
||||
title="Manual verification required"
|
||||
>
|
||||
<FlaskConical className="w-3 h-3" />
|
||||
<Hand className="w-3 h-3" />
|
||||
<span>Manual</span>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className="p-3 pb-2">
|
||||
{isCurrentAutoTask && (
|
||||
<div className="absolute top-2 right-2 flex items-center gap-2 bg-running-indicator/20 border border-running-indicator rounded px-2 py-0.5">
|
||||
<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">
|
||||
Running...
|
||||
</span>
|
||||
{feature.startedAt && (
|
||||
<CountUpTimer
|
||||
startedAt={feature.startedAt}
|
||||
@@ -238,17 +240,36 @@ export function KanbanCard({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Show timer for in_progress cards that aren't currently running */}
|
||||
{!isCurrentAutoTask &&
|
||||
feature.status === "in_progress" &&
|
||||
feature.startedAt && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<CountUpTimer
|
||||
startedAt={feature.startedAt}
|
||||
className="text-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isCurrentAutoTask && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 hover:bg-white/10"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-testid={`menu-${feature.id}`}
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteClick(e as unknown as React.MouseEvent);
|
||||
}}
|
||||
data-testid={`delete-feature-${feature.id}`}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start gap-2">
|
||||
{isDraggable && (
|
||||
<div
|
||||
@@ -295,29 +316,6 @@ export function KanbanCard({
|
||||
)}
|
||||
|
||||
{/* Agent Info Panel - shows for in_progress, waiting_approval, verified */}
|
||||
{/* Standard mode: Only show progress bar */}
|
||||
{showProgressBar &&
|
||||
!showAgentInfo &&
|
||||
feature.status !== "backlog" &&
|
||||
agentInfo &&
|
||||
(isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="w-full h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detailed mode: Show all agent info */}
|
||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||
<div className="mb-3 space-y-2">
|
||||
@@ -346,39 +344,6 @@ export function KanbanCard({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Indicator */}
|
||||
{(isCurrentAutoTask || feature.status === "in_progress") && (
|
||||
<div className="space-y-1">
|
||||
<div className="w-full h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="w-full h-full bg-primary transition-transform duration-500 ease-out origin-left"
|
||||
style={{
|
||||
transform: `translateX(${
|
||||
agentInfo.progressPercentage - 100
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tools
|
||||
</span>
|
||||
{agentInfo.lastToolUsed && (
|
||||
<span
|
||||
className="text-muted-foreground truncate max-w-[80px]"
|
||||
title={agentInfo.lastToolUsed}
|
||||
>
|
||||
{agentInfo.lastToolUsed}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span>{Math.round(agentInfo.progressPercentage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task List Progress (if todos found) */}
|
||||
{agentInfo.todos.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
@@ -498,8 +463,8 @@ export function KanbanCard({
|
||||
}}
|
||||
data-testid={`view-output-${feature.id}`}
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
View Output
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Logs
|
||||
</Button>
|
||||
)}
|
||||
{onForceStop && (
|
||||
@@ -526,7 +491,7 @@ export function KanbanCard({
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-action-verify hover:bg-action-verify-hover"
|
||||
className="flex-1 h-7 text-xs bg-primary hover:bg-primary/90"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualVerify();
|
||||
@@ -576,23 +541,30 @@ export function KanbanCard({
|
||||
}}
|
||||
data-testid={`view-output-inprogress-${feature.id}`}
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
Output
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Logs
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={handleDeleteClick}
|
||||
data-testid={`delete-inprogress-feature-${feature.id}`}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "verified" && (
|
||||
<>
|
||||
{/* 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-verified-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Logs
|
||||
</Button>
|
||||
)}
|
||||
{/* Move back button for skipTests verified features */}
|
||||
{feature.skipTests && onMoveBackToInProgress && (
|
||||
<Button
|
||||
@@ -622,25 +594,32 @@ export function KanbanCard({
|
||||
<Edit className="w-3 h-3 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={handleDeleteClick}
|
||||
data-testid={`delete-feature-${feature.id}`}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</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
|
||||
variant="default"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-action-followup hover:bg-action-followup-hover"
|
||||
className="flex-1 h-7 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFollowUp();
|
||||
@@ -656,7 +635,7 @@ export function KanbanCard({
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-action-commit hover:bg-action-commit-hover"
|
||||
className="flex-1 h-7 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCommit();
|
||||
@@ -667,19 +646,25 @@ export function KanbanCard({
|
||||
Commit
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={handleDeleteClick}
|
||||
data-testid={`delete-waiting-feature-${feature.id}`}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "backlog" && (
|
||||
<>
|
||||
{onViewOutput && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewOutput();
|
||||
}}
|
||||
data-testid={`view-logs-backlog-${feature.id}`}
|
||||
>
|
||||
<FileText className="w-3 h-3 mr-1" />
|
||||
Logs
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -693,15 +678,6 @@ export function KanbanCard({
|
||||
<Edit className="w-3 h-3 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={handleDeleteClick}
|
||||
data-testid={`delete-feature-${feature.id}`}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -796,150 +796,162 @@ export function SettingsView() {
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground">Theme</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<button
|
||||
<Button
|
||||
variant={theme === "dark" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("dark")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "dark"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="dark-mode-button"
|
||||
>
|
||||
<Moon className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Dark</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "light" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("light")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "light"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="light-mode-button"
|
||||
>
|
||||
<Sun className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Light</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "retro" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("retro")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "retro"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="retro-mode-button"
|
||||
>
|
||||
<Terminal className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Retro</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "dracula" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("dracula")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "dracula"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="dracula-mode-button"
|
||||
>
|
||||
<Ghost className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Dracula</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "nord" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("nord")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "nord"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="nord-mode-button"
|
||||
>
|
||||
<Snowflake className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Nord</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "monokai" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("monokai")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "monokai"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="monokai-mode-button"
|
||||
>
|
||||
<Flame className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Monokai</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "tokyonight" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("tokyonight")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "tokyonight"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="tokyonight-mode-button"
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Tokyo Night</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "solarized" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("solarized")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "solarized"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="solarized-mode-button"
|
||||
>
|
||||
<Eclipse className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Solarized</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "gruvbox" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("gruvbox")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "gruvbox"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="gruvbox-mode-button"
|
||||
>
|
||||
<Trees className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Gruvbox</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "catppuccin" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("catppuccin")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "catppuccin"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="catppuccin-mode-button"
|
||||
>
|
||||
<Cat className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Catppuccin</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "onedark" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("onedark")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "onedark"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="onedark-mode-button"
|
||||
>
|
||||
<Atom className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">One Dark</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={theme === "synthwave" ? "secondary" : "outline"}
|
||||
onClick={() => setTheme("synthwave")}
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 rounded-lg border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
|
||||
theme === "synthwave"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="synthwave-mode-button"
|
||||
>
|
||||
<Radio className="w-4 h-4" />
|
||||
<span className="font-medium text-sm">Synthwave</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -962,12 +974,13 @@ export function SettingsView() {
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground">Detail Level</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
<Button
|
||||
variant={kanbanCardDetailLevel === "minimal" ? "secondary" : "outline"}
|
||||
onClick={() => setKanbanCardDetailLevel("minimal")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||
kanbanCardDetailLevel === "minimal"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="kanban-detail-minimal"
|
||||
>
|
||||
@@ -976,13 +989,14 @@ export function SettingsView() {
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Title & category only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={kanbanCardDetailLevel === "standard" ? "secondary" : "outline"}
|
||||
onClick={() => setKanbanCardDetailLevel("standard")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||
kanbanCardDetailLevel === "standard"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="kanban-detail-standard"
|
||||
>
|
||||
@@ -991,13 +1005,14 @@ export function SettingsView() {
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Steps & progress
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant={kanbanCardDetailLevel === "detailed" ? "secondary" : "outline"}
|
||||
onClick={() => setKanbanCardDetailLevel("detailed")}
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 rounded-lg border transition-all ${
|
||||
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
|
||||
kanbanCardDetailLevel === "detailed"
|
||||
? "bg-accent border-brand-500 text-foreground"
|
||||
: "bg-input border-border text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
? "border-brand-500 ring-1 ring-brand-500/50"
|
||||
: ""
|
||||
}`}
|
||||
data-testid="kanban-detail-detailed"
|
||||
>
|
||||
@@ -1006,7 +1021,7 @@ export function SettingsView() {
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Model, tools & tasks
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Minimal:</strong> Shows only title and category
|
||||
|
||||
@@ -25,7 +25,6 @@ import { initializeProject } from "@/lib/project-init";
|
||||
import {
|
||||
FolderOpen,
|
||||
Plus,
|
||||
Cpu,
|
||||
Folder,
|
||||
Clock,
|
||||
Sparkles,
|
||||
@@ -284,8 +283,12 @@ export function WelcomeView() {
|
||||
<div className="flex-shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="px-8 py-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
||||
<Cpu className="w-5 h-5 text-primary-foreground" />
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">
|
||||
|
||||
Reference in New Issue
Block a user