mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
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:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user