feat: add new themes and improve UI components

- Introduced multiple new themes: retro, dracula, nord, monokai, tokyonight, solarized, gruvbox, catppuccin, onedark, and synthwave.
- Updated global CSS to support new themes and added custom variants for theme-specific styles.
- Enhanced layout and sidebar components with improved styling and responsiveness.
- Refactored button and slider components for better visual consistency and added an animated outline variant.
- Improved various views (e.g., settings, welcome, context) with updated styles and better user experience.

This update enhances the overall aesthetic and usability of the application, providing users with more customization options.
This commit is contained in:
Cody Seibert
2025-12-09 18:51:06 -05:00
parent 66951f2b94
commit 6f3bf2f6b6
19 changed files with 2212 additions and 500 deletions

View File

@@ -16,7 +16,12 @@ import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useAppStore, Feature, FeatureImage, FeatureImagePath } from "@/store/app-store";
import {
useAppStore,
Feature,
FeatureImage,
FeatureImagePath,
} from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { cn } from "@/lib/utils";
import {
@@ -31,7 +36,10 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { CategoryAutocomplete } from "@/components/ui/category-autocomplete";
import { FeatureImageUpload } from "@/components/ui/feature-image-upload";
import { DescriptionImageDropZone, FeatureImagePath as DescriptionImagePath } from "@/components/ui/description-image-dropzone";
import {
DescriptionImageDropZone,
FeatureImagePath as DescriptionImagePath,
} from "@/components/ui/description-image-dropzone";
import {
Dialog,
DialogContent,
@@ -44,7 +52,20 @@ import { KanbanColumn } from "./kanban-column";
import { KanbanCard } from "./kanban-card";
import { AutoModeLog } from "./auto-mode-log";
import { AgentOutputModal } from "./agent-output-modal";
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward, FlaskConical, CheckCircle2 } from "lucide-react";
import {
Plus,
RefreshCw,
Play,
StopCircle,
Loader2,
ChevronUp,
ChevronDown,
Users,
Trash2,
FastForward,
FlaskConical,
CheckCircle2,
} from "lucide-react";
import { toast } from "sonner";
import { Slider } from "@/components/ui/slider";
import { Checkbox } from "@/components/ui/checkbox";
@@ -92,8 +113,11 @@ export function BoardView() {
const [showActivityLog, setShowActivityLog] = useState(false);
const [showOutputModal, setShowOutputModal] = useState(false);
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(new Set());
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] = useState(false);
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(
new Set()
);
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] =
useState(false);
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
// Make current project available globally for modal
@@ -125,39 +149,36 @@ export function BoardView() {
const startNextFeaturesRef = useRef<() => void>(() => {});
// Keyboard shortcuts for this view
const boardShortcuts: KeyboardShortcut[] = useMemo(
() => {
const shortcuts: KeyboardShortcut[] = [
{
key: ACTION_SHORTCUTS.addFeature,
action: () => setShowAddDialog(true),
description: "Add new feature",
},
{
key: ACTION_SHORTCUTS.startNext,
action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog",
},
];
const boardShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [
{
key: ACTION_SHORTCUTS.addFeature,
action: () => setShowAddDialog(true),
description: "Add new feature",
},
{
key: ACTION_SHORTCUTS.startNext,
action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog",
},
];
// Add shortcuts for in-progress cards (1-9 and 0 for 10th)
inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => {
// Keys 1-9 for first 9 cards, 0 for 10th card
const key = index === 9 ? "0" : String(index + 1);
shortcuts.push({
key,
action: () => {
setOutputFeature(feature);
setShowOutputModal(true);
},
description: `View output for in-progress card ${index + 1}`,
});
// Add shortcuts for in-progress cards (1-9 and 0 for 10th)
inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => {
// Keys 1-9 for first 9 cards, 0 for 10th card
const key = index === 9 ? "0" : String(index + 1);
shortcuts.push({
key,
action: () => {
setOutputFeature(feature);
setShowOutputModal(true);
},
description: `View output for in-progress card ${index + 1}`,
});
});
return shortcuts;
},
[inProgressFeaturesForShortcuts]
);
return shortcuts;
}, [inProgressFeaturesForShortcuts]);
useKeyboardShortcuts(boardShortcuts);
// Prevent hydration issues
@@ -207,7 +228,9 @@ export function BoardView() {
// If project switched, clear features first to prevent cross-contamination
if (previousPath !== null && currentPath !== previousPath) {
console.log(`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features`);
console.log(
`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}, clearing features`
);
isSwitchingProjectRef.current = true;
setFeatures([]);
setPersistedCategories([]); // Also clear categories
@@ -225,14 +248,12 @@ export function BoardView() {
if (result.success && result.content) {
const parsed = JSON.parse(result.content);
const featuresWithIds = parsed.map(
(f: any, index: number) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
status: f.status || "backlog",
startedAt: f.startedAt, // Preserve startedAt timestamp
})
);
const featuresWithIds = parsed.map((f: any, index: number) => ({
...f,
id: f.id || `feature-${index}-${Date.now()}`,
status: f.status || "backlog",
startedAt: f.startedAt, // Preserve startedAt timestamp
}));
setFeatures(featuresWithIds);
}
} catch (error) {
@@ -270,33 +291,36 @@ export function BoardView() {
}, [currentProject]);
// Save a new category to the persisted categories file
const saveCategory = useCallback(async (category: string) => {
if (!currentProject || !category.trim()) return;
const saveCategory = useCallback(
async (category: string) => {
if (!currentProject || !category.trim()) return;
try {
const api = getElectronAPI();
try {
const api = getElectronAPI();
// Read existing categories
let categories: string[] = [...persistedCategories];
// Read existing categories
let categories: string[] = [...persistedCategories];
// Add new category if it doesn't exist
if (!categories.includes(category)) {
categories.push(category);
categories.sort(); // Keep sorted
// Add new category if it doesn't exist
if (!categories.includes(category)) {
categories.push(category);
categories.sort(); // Keep sorted
// Write back to file
await api.writeFile(
`${currentProject.path}/.automaker/categories.json`,
JSON.stringify(categories, null, 2)
);
// Write back to file
await api.writeFile(
`${currentProject.path}/.automaker/categories.json`,
JSON.stringify(categories, null, 2)
);
// Update state
setPersistedCategories(categories);
// Update state
setPersistedCategories(categories);
}
} catch (error) {
console.error("Failed to save category:", error);
}
} catch (error) {
console.error("Failed to save category:", error);
}
}, [currentProject, persistedCategories]);
},
[currentProject, persistedCategories]
);
// Auto-show activity log when auto mode starts
useEffect(() => {
@@ -339,7 +363,10 @@ export function BoardView() {
const status = await api.autoMode.status();
if (status.success && status.runningFeatures) {
console.log("[Board] Syncing running tasks from backend:", status.runningFeatures);
console.log(
"[Board] Syncing running tasks from backend:",
status.runningFeatures
);
// Clear existing running tasks and add the actual running ones
const { clearRunningTasks, addRunningTask } = useAppStore.getState();
@@ -361,7 +388,9 @@ export function BoardView() {
// Check which features have context files
useEffect(() => {
const checkAllContexts = async () => {
const inProgressFeatures = features.filter((f) => f.status === "in_progress");
const inProgressFeatures = features.filter(
(f) => f.status === "in_progress"
);
const contextChecks = await Promise.all(
inProgressFeatures.map(async (f) => ({
id: f.id,
@@ -447,7 +476,9 @@ export function BoardView() {
if (draggedFeature.status !== "backlog") {
// Only allow dragging in_progress/verified if it's a skipTests feature and not currently running
if (!draggedFeature.skipTests || isRunningTask) {
console.log("[Board] Cannot drag feature - TDD feature or currently running");
console.log(
"[Board] Cannot drag feature - TDD feature or currently running"
);
return;
}
}
@@ -472,10 +503,16 @@ export function BoardView() {
if (targetStatus === draggedFeature.status) return;
// Check concurrency limit before moving to in_progress (only for backlog -> in_progress and if running agent)
if (targetStatus === "in_progress" && draggedFeature.status === "backlog" && !autoMode.canStartNewTask) {
if (
targetStatus === "in_progress" &&
draggedFeature.status === "backlog" &&
!autoMode.canStartNewTask
) {
console.log("[Board] Cannot start new task - at max concurrency limit");
toast.error("Concurrency limit reached", {
description: `You can only have ${autoMode.maxConcurrency} task${autoMode.maxConcurrency > 1 ? "s" : ""} running at a time. Wait for a task to complete or increase the limit.`,
description: `You can only have ${autoMode.maxConcurrency} task${
autoMode.maxConcurrency > 1 ? "s" : ""
} running at a time. Wait for a task to complete or increase the limit.`,
});
return;
}
@@ -485,7 +522,10 @@ export function BoardView() {
// From backlog
if (targetStatus === "in_progress") {
// Update with startedAt timestamp
updateFeature(featureId, { status: targetStatus, startedAt: new Date().toISOString() });
updateFeature(featureId, {
status: targetStatus,
startedAt: new Date().toISOString(),
});
console.log("[Board] Feature moved to in_progress, starting agent...");
await handleRunFeature(draggedFeature);
} else {
@@ -493,23 +533,41 @@ export function BoardView() {
}
} else if (draggedFeature.skipTests) {
// skipTests feature being moved between in_progress and verified
if (targetStatus === "verified" && draggedFeature.status === "in_progress") {
if (
targetStatus === "verified" &&
draggedFeature.status === "in_progress"
) {
// Manual verify via drag
moveFeature(featureId, "verified");
toast.success("Feature verified", {
description: `Marked as verified: ${draggedFeature.description.slice(0, 50)}${draggedFeature.description.length > 50 ? "..." : ""}`,
description: `Marked as verified: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (targetStatus === "in_progress" && draggedFeature.status === "verified") {
} else if (
targetStatus === "in_progress" &&
draggedFeature.status === "verified"
) {
// Move back to in_progress
updateFeature(featureId, { status: "in_progress", startedAt: new Date().toISOString() });
updateFeature(featureId, {
status: "in_progress",
startedAt: new Date().toISOString(),
});
toast.info("Feature moved back", {
description: `Moved back to In Progress: ${draggedFeature.description.slice(0, 50)}${draggedFeature.description.length > 50 ? "..." : ""}`,
description: `Moved back to In Progress: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
} else if (targetStatus === "backlog") {
// Allow moving skipTests cards back to backlog
moveFeature(featureId, "backlog");
toast.info("Feature moved to backlog", {
description: `Moved to Backlog: ${draggedFeature.description.slice(0, 50)}${draggedFeature.description.length > 50 ? "..." : ""}`,
description: `Moved to Backlog: ${draggedFeature.description.slice(
0,
50
)}${draggedFeature.description.length > 50 ? "..." : ""}`,
});
}
}
@@ -528,7 +586,14 @@ export function BoardView() {
});
// Persist the category
saveCategory(category);
setNewFeature({ category: "", description: "", steps: [""], images: [], imagePaths: [], skipTests: false });
setNewFeature({
category: "",
description: "",
steps: [""],
images: [],
imagePaths: [],
skipTests: false,
});
setShowAddDialog(false);
};
@@ -560,7 +625,10 @@ export function BoardView() {
try {
await autoMode.stopFeature(featureId);
toast.success("Agent stopped", {
description: `Stopped and deleted: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Stopped and deleted: ${feature.description.slice(
0,
50
)}${feature.description.length > 50 ? "..." : ""}`,
});
} catch (error) {
console.error("[Board] Error stopping feature before delete:", error);
@@ -609,7 +677,10 @@ export function BoardView() {
const handleVerifyFeature = async (feature: Feature) => {
if (!currentProject) return;
console.log("[Board] Verifying feature:", { id: feature.id, description: feature.description });
console.log("[Board] Verifying feature:", {
id: feature.id,
description: feature.description,
});
try {
const api = getElectronAPI();
@@ -641,7 +712,10 @@ export function BoardView() {
const handleResumeFeature = async (feature: Feature) => {
if (!currentProject) return;
console.log("[Board] Resuming feature:", { id: feature.id, description: feature.description });
console.log("[Board] Resuming feature:", {
id: feature.id,
description: feature.description,
});
try {
const api = getElectronAPI();
@@ -672,19 +746,33 @@ export function BoardView() {
// Manual verification handler for skipTests features
const handleManualVerify = (feature: Feature) => {
console.log("[Board] Manually verifying feature:", { id: feature.id, description: feature.description });
console.log("[Board] Manually verifying feature:", {
id: feature.id,
description: feature.description,
});
moveFeature(feature.id, "verified");
toast.success("Feature verified", {
description: `Marked as verified: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Marked as verified: ${feature.description.slice(0, 50)}${
feature.description.length > 50 ? "..." : ""
}`,
});
};
// Move feature back to in_progress from verified (for skipTests features)
const handleMoveBackToInProgress = (feature: Feature) => {
console.log("[Board] Moving feature back to in_progress:", { id: feature.id, description: feature.description });
updateFeature(feature.id, { status: "in_progress", startedAt: new Date().toISOString() });
console.log("[Board] Moving feature back to in_progress:", {
id: feature.id,
description: feature.description,
});
updateFeature(feature.id, {
status: "in_progress",
startedAt: new Date().toISOString(),
});
toast.info("Feature moved back", {
description: `Moved back to In Progress: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Moved back to In Progress: ${feature.description.slice(
0,
50
)}${feature.description.length > 50 ? "..." : ""}`,
});
};
@@ -727,28 +815,31 @@ export function BoardView() {
};
// Handle number key press when output modal is open
const handleOutputModalNumberKeyPress = useCallback((key: string) => {
// Convert key to index: 1-9 -> 0-8, 0 -> 9
const index = key === "0" ? 9 : parseInt(key, 10) - 1;
const handleOutputModalNumberKeyPress = useCallback(
(key: string) => {
// Convert key to index: 1-9 -> 0-8, 0 -> 9
const index = key === "0" ? 9 : parseInt(key, 10) - 1;
// Get the feature at that index from in-progress features
const targetFeature = inProgressFeaturesForShortcuts[index];
// Get the feature at that index from in-progress features
const targetFeature = inProgressFeaturesForShortcuts[index];
if (!targetFeature) {
// No feature at this index, do nothing
return;
}
if (!targetFeature) {
// No feature at this index, do nothing
return;
}
// If pressing the same number key as the currently open feature, close the modal
if (targetFeature.id === outputFeature?.id) {
setShowOutputModal(false);
}
// If pressing a different number key, switch to that feature's output
else {
setOutputFeature(targetFeature);
// Modal stays open, just showing different content
}
}, [inProgressFeaturesForShortcuts, outputFeature?.id]);
// If pressing the same number key as the currently open feature, close the modal
if (targetFeature.id === outputFeature?.id) {
setShowOutputModal(false);
}
// If pressing a different number key, switch to that feature's output
else {
setOutputFeature(targetFeature);
// Modal stays open, just showing different content
}
},
[inProgressFeaturesForShortcuts, outputFeature?.id]
);
const handleForceStopFeature = async (feature: Feature) => {
try {
@@ -756,12 +847,15 @@ export function BoardView() {
// Move the feature back to backlog status after stopping
moveFeature(feature.id, "backlog");
toast.success("Agent stopped", {
description: `Stopped working on: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
description: `Stopped working on: ${feature.description.slice(0, 50)}${
feature.description.length > 50 ? "..." : ""
}`,
});
} catch (error) {
console.error("[Board] Error stopping feature:", error);
toast.error("Failed to stop agent", {
description: error instanceof Error ? error.message : "An error occurred",
description:
error instanceof Error ? error.message : "An error occurred",
});
}
};
@@ -773,7 +867,9 @@ export function BoardView() {
if (availableSlots <= 0) {
toast.error("Concurrency limit reached", {
description: `You can only have ${maxConcurrency} task${maxConcurrency > 1 ? "s" : ""} running at a time. Wait for a task to complete or increase the limit.`,
description: `You can only have ${maxConcurrency} task${
maxConcurrency > 1 ? "s" : ""
} running at a time. Wait for a task to complete or increase the limit.`,
});
return;
}
@@ -789,14 +885,28 @@ export function BoardView() {
for (const feature of featuresToStart) {
// Update the feature status with startedAt timestamp
updateFeature(feature.id, { status: "in_progress", startedAt: new Date().toISOString() });
updateFeature(feature.id, {
status: "in_progress",
startedAt: new Date().toISOString(),
});
// Start the agent for this feature
await handleRunFeature(feature);
}
toast.success(`Started ${featuresToStart.length} feature${featuresToStart.length > 1 ? "s" : ""}`, {
description: featuresToStart.map((f) => f.description.slice(0, 30) + (f.description.length > 30 ? "..." : "")).join(", "),
});
toast.success(
`Started ${featuresToStart.length} feature${
featuresToStart.length > 1 ? "s" : ""
}`,
{
description: featuresToStart
.map(
(f) =>
f.description.slice(0, 30) +
(f.description.length > 30 ? "..." : "")
)
.join(", "),
}
);
}, [features, maxConcurrency, runningAutoTasks.length, updateFeature]);
// Update ref when handleStartNextFeatures changes
@@ -832,7 +942,7 @@ export function BoardView() {
data-testid="board-view"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
<div>
<h1 className="text-xl font-bold">Kanban Board</h1>
<p className="text-sm text-muted-foreground">{currentProject.name}</p>
@@ -841,10 +951,10 @@ export function BoardView() {
{/* Concurrency Slider - only show after mount to prevent hydration issues */}
{isMounted && (
<div
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10"
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border"
data-testid="concurrency-slider-container"
>
<Users className="w-4 h-4 text-zinc-400" />
<Users className="w-4 h-4 text-muted-foreground" />
<Slider
value={[maxConcurrency]}
onValueChange={(value) => setMaxConcurrency(value[0])}
@@ -855,7 +965,7 @@ export function BoardView() {
data-testid="concurrency-slider"
/>
<span
className="text-sm text-zinc-400 min-w-[2ch] text-center"
className="text-sm text-muted-foreground min-w-[2ch] text-center"
data-testid="concurrency-value"
>
{maxConcurrency}
@@ -878,11 +988,10 @@ export function BoardView() {
</Button>
) : (
<Button
variant="default"
variant="secondary"
size="sm"
onClick={() => autoMode.start()}
data-testid="start-auto-mode"
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
>
<Play className="w-4 h-4 mr-2" />
Auto Mode
@@ -928,110 +1037,117 @@ 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"
)}>
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
<div
className={cn(
"flex-1 overflow-x-auto p-4",
showActivityLog && "transition-all"
)}
>
<div className="flex gap-4 h-full min-w-max">
{COLUMNS.map((column) => {
const columnFeatures = getColumnFeatures(column.id);
return (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
color={column.color}
count={columnFeatures.length}
isDoubleWidth={column.id === "in_progress"}
headerAction={
column.id === "verified" && columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => setShowDeleteAllVerifiedDialog(true)}
data-testid="delete-all-verified-button"
>
<Trash2 className="w-3 h-3 mr-1" />
Delete All
</Button>
) : column.id === "backlog" && columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={handleStartNextFeatures}
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Start Next
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-white/10 border border-white/20">
{ACTION_SHORTCUTS.startNext}
</span>
</Button>
) : undefined
}
>
<SortableContext
items={columnFeatures.map((f) => f.id)}
strategy={verticalListSortingStrategy}
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex gap-4 h-full min-w-max">
{COLUMNS.map((column) => {
const columnFeatures = getColumnFeatures(column.id);
return (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
color={column.color}
count={columnFeatures.length}
isDoubleWidth={column.id === "in_progress"}
headerAction={
column.id === "verified" && columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => setShowDeleteAllVerifiedDialog(true)}
data-testid="delete-all-verified-button"
>
<Trash2 className="w-3 h-3 mr-1" />
Delete All
</Button>
) : column.id === "backlog" &&
columnFeatures.length > 0 ? (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
onClick={handleStartNextFeatures}
data-testid="start-next-button"
>
<FastForward className="w-3 h-3 mr-1" />
Start Next
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-white/10 border border-white/20">
{ACTION_SHORTCUTS.startNext}
</span>
</Button>
) : undefined
}
>
{columnFeatures.map((feature, index) => {
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
let shortcutKey: string | undefined;
if (column.id === "in_progress" && index < 10) {
shortcutKey = index === 9 ? "0" : String(index + 1);
}
return (
<KanbanCard
key={feature.id}
feature={feature}
onEdit={() => setEditingFeature(feature)}
onDelete={() => handleDeleteFeature(feature.id)}
onViewOutput={() => handleViewOutput(feature)}
onVerify={() => handleVerifyFeature(feature)}
onResume={() => handleResumeFeature(feature)}
onForceStop={() => handleForceStopFeature(feature)}
onManualVerify={() => handleManualVerify(feature)}
onMoveBackToInProgress={() => handleMoveBackToInProgress(feature)}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
shortcutKey={shortcutKey}
/>
);
})}
</SortableContext>
</KanbanColumn>
);
})}
</div>
<SortableContext
items={columnFeatures.map((f) => f.id)}
strategy={verticalListSortingStrategy}
>
{columnFeatures.map((feature, index) => {
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
let shortcutKey: string | undefined;
if (column.id === "in_progress" && index < 10) {
shortcutKey = index === 9 ? "0" : String(index + 1);
}
return (
<KanbanCard
key={feature.id}
feature={feature}
onEdit={() => setEditingFeature(feature)}
onDelete={() => handleDeleteFeature(feature.id)}
onViewOutput={() => handleViewOutput(feature)}
onVerify={() => handleVerifyFeature(feature)}
onResume={() => handleResumeFeature(feature)}
onForceStop={() => handleForceStopFeature(feature)}
onManualVerify={() => handleManualVerify(feature)}
onMoveBackToInProgress={() =>
handleMoveBackToInProgress(feature)
}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(
feature.id
)}
shortcutKey={shortcutKey}
/>
);
})}
</SortableContext>
</KanbanColumn>
);
})}
</div>
<DragOverlay>
{activeFeature && (
<Card className="w-72 opacity-90 rotate-3 shadow-xl">
<CardHeader className="p-3">
<CardTitle className="text-sm">
{activeFeature.description}
</CardTitle>
<CardDescription className="text-xs">
{activeFeature.category}
</CardDescription>
</CardHeader>
</Card>
)}
</DragOverlay>
</DndContext>
<DragOverlay>
{activeFeature && (
<Card className="w-72 opacity-90 rotate-3 shadow-xl">
<CardHeader className="p-3">
<CardTitle className="text-sm">
{activeFeature.description}
</CardTitle>
<CardDescription className="text-xs">
{activeFeature.category}
</CardDescription>
</CardHeader>
</Card>
)}
</DragOverlay>
</DndContext>
</div>
{/* Activity Log Panel */}
{showActivityLog && (
<div className="w-96 border-l border-white/10 flex-shrink-0">
<div className="w-96 border-l border-border flex-shrink-0">
<AutoModeLog onClose={() => setShowActivityLog(false)} />
</div>
)}
@@ -1042,7 +1158,11 @@ export function BoardView() {
<DialogContent
data-testid="add-feature-dialog"
onKeyDown={(e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && newFeature.description) {
if (
(e.metaKey || e.ctrlKey) &&
e.key === "Enter" &&
newFeature.description
) {
e.preventDefault();
handleAddFeature();
}
@@ -1128,7 +1248,8 @@ export function BoardView() {
</div>
</div>
<p className="text-xs text-muted-foreground">
When enabled, this feature will require manual verification instead of automated TDD.
When enabled, this feature will require manual verification
instead of automated TDD.
</p>
</div>
<DialogFooter>
@@ -1227,19 +1348,26 @@ export function BoardView() {
id="edit-skip-tests"
checked={editingFeature.skipTests ?? false}
onCheckedChange={(checked) =>
setEditingFeature({ ...editingFeature, skipTests: checked === true })
setEditingFeature({
...editingFeature,
skipTests: checked === true,
})
}
data-testid="edit-skip-tests-checkbox"
/>
<div className="flex items-center gap-2">
<Label htmlFor="edit-skip-tests" className="text-sm cursor-pointer">
<Label
htmlFor="edit-skip-tests"
className="text-sm cursor-pointer"
>
Skip automated testing
</Label>
<FlaskConical className="w-3.5 h-3.5 text-muted-foreground" />
</div>
</div>
<p className="text-xs text-muted-foreground">
When enabled, this feature will require manual verification instead of automated TDD.
When enabled, this feature will require manual verification
instead of automated TDD.
</p>
</div>
)}
@@ -1267,21 +1395,29 @@ export function BoardView() {
/>
{/* Delete All Verified Dialog */}
<Dialog open={showDeleteAllVerifiedDialog} onOpenChange={setShowDeleteAllVerifiedDialog}>
<Dialog
open={showDeleteAllVerifiedDialog}
onOpenChange={setShowDeleteAllVerifiedDialog}
>
<DialogContent data-testid="delete-all-verified-dialog">
<DialogHeader>
<DialogTitle>Delete All Verified Features</DialogTitle>
<DialogDescription>
Are you sure you want to delete all verified features? This action cannot be undone.
Are you sure you want to delete all verified features? This action
cannot be undone.
{getColumnFeatures("verified").length > 0 && (
<span className="block mt-2 text-yellow-500">
{getColumnFeatures("verified").length} feature(s) will be deleted.
{getColumnFeatures("verified").length} feature(s) will be
deleted.
</span>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowDeleteAllVerifiedDialog(false)}>
<Button
variant="ghost"
onClick={() => setShowDeleteAllVerifiedDialog(false)}
>
Cancel
</Button>
<Button
@@ -1297,7 +1433,10 @@ export function BoardView() {
try {
await autoMode.stopFeature(feature.id);
} catch (error) {
console.error("[Board] Error stopping feature before delete:", error);
console.error(
"[Board] Error stopping feature before delete:",
error
);
}
}