feat(kanban): Persist categories to .automaker/categories.json

Categories are now saved to a categories.json file in the .automaker
directory. This ensures categories persist even when all feature cards
are deleted. The category autocomplete now shows suggestions from both
existing features AND the persisted categories file.

- Add persistedCategories state and load/save functions in board-view
- Merge persisted categories with feature categories for suggestions
- Auto-save categories when adding or editing features
- Update mock Electron API to handle categories.json file

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cody Seibert
2025-12-09 08:34:54 -05:00
parent 2aa601881e
commit 188de1bbca
3 changed files with 172 additions and 108 deletions

View File

@@ -1,100 +1,42 @@
[
{
"id": "feature-1765260864296-98yunv0vj",
"category": "Kanban",
"description": "Remove drag icon from cards when in in progress or verified. also add a timer that tracks how long it has been since the agent started, a count up timer basically formatted 00:00",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765262225700-q2rkue6l8",
"category": "Context",
"description": "Add Context File should show a file name and a textarea for the context info, that text area should allow drag n drop for txt files and .md files which the system will parse and put into the text area",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765262348401-hivjg6vuq",
"category": "Kanban",
"description": "Make in progress column double width so that the cards display 2 columns masonry layout",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765262430461-vennhg2b5",
"id": "feature-1765286925345-4eu5miocn",
"category": "Core",
"description": "When the electron ui refreshes, it often redirects me back to the overview, remeber my last route and restore on app load",
"description": "After a category has been created we should persist that somewhere such as in the .automaker directory in a .json file so that all the categories of future items I add in will persist even if I delete all the cards.",
"steps": [],
"status": "verified"
"status": "in_progress",
"startedAt": "2025-12-09T13:30:14.509Z"
},
{
"id": "feature-1765263521367-vse4n57j8",
"category": "Spec Editor",
"description": "The spec editor no longer shows the content of the file, check the path if correct and using the .automaker directory",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765263773317-k2cmvg9qh",
"id": "feature-1765287004835-d2c5aqdkr",
"category": "Core",
"description": "When opening a new project, verify the .automaker directory is created with necessary files and kick off an agent to analyze the project, refactor the app_spec to describe the project and it's tech stack, and any features currently implemented, also define a blank feature_list.json, create necessary context and agents-context directories, and coding_prompt.md.",
"description": "I need to add the ability on the Kanban cards to enable or disable if it's going to do the whole test driven development approach. Sometimes the task is so easy I don't need it to write tests for, but... And if it is a task that did not have a test, a user should manually be able to click a verified button when it's in the in progress column and the agent is done. I'll have to manually verify it. So this might change the logic a little bit to allow dragging of cards when they aren't tested automatically from the in progress column back to verified and from verified back to in progress. But keep the existing functionality if it is a test automated card and prevent the dragging.",
"steps": [],
"status": "verified"
"status": "in_progress",
"startedAt": "2025-12-09T13:30:17.323Z"
},
{
"id": "feature-1765263831805-mr6mduv8p",
"category": "Core",
"description": "remove claude-progress from anywhere in code or prompts as it's no longer needed",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765264341539-km1238av9",
"category": "Core",
"description": "remove the code view link",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765264472003-ikdarjmlw",
"category": "Core",
"description": "Add shortcuts keys to all left navigation links, then add shortcuts to the add buttons on the routes (such as kanbam add feature). mske sure they don't mess with normal input or textarea typing or typeaheads. display the shortkey in link or button for users to know (K)",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765264941688-omfcpy7no",
"id": "feature-1765287091626-ceoj6xld8",
"category": "Kanban",
"description": "For the first 10 in progress cards, add shortcut keys 1 through 0 on the keyboard for opening their output modal",
"description": "Show a confirmed dialog when I click on the trash icon for deleting a card. Use the existing dialog that we have in the components directory for this.",
"steps": [],
"status": "verified"
"status": "in_progress",
"startedAt": "2025-12-09T13:32:24.947Z"
},
{
"id": "feature-1765265001317-4eyqyif9z",
"id": "feature-1765287114711-fgypwhnvt",
"category": "Kanban",
"description": "Add a delete all button in the verified column header which runs through all verified cards and deletes them with the exact same delete actions. remember to show a confirm delete confirmation dialog before actually deleting.",
"description": "When adding a new feature inside the modal there's an add feature button. Can you add a shortcut of shift? Enter which if you click shift enter I'll automatically add it in",
"steps": [],
"status": "verified"
"status": "in_progress",
"startedAt": "2025-12-09T13:32:25.263Z"
},
{
"id": "feature-1765265036114-9oong1mrv",
"id": "feature-1765287141131-dz489etgj",
"category": "Kanban",
"description": "Remove the refresh button from the headers, we should need to ever manually refresh anything if our app is well designed",
"description": "When I edit a card, it's showing an input for the description refactor to also show a text area for description like we do on the add card, add feature card.",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765265099914-71eq4x4yl",
"category": "Core",
"description": "Add a ` shortcut to toggle the left side panel (on hover of the toggle show a tool tip with the shortcut info)",
"steps": [],
"status": "verified"
},
{
"id": "feature-1765265179876-5zcrlncdf",
"category": "Kanban",
"description": "Add a button in the backlog header which will just take the top cards and put them into the in progress board (up to the limit of the concurrency of course) so that a user doesn't have to drag each on individually, figure out the best name for it. give it a shortcut as well",
"steps": [],
"status": "verified"
"status": "in_progress",
"startedAt": "2025-12-09T13:32:25.469Z"
}
]

View File

@@ -43,9 +43,10 @@ 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 } 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";
import { useAutoMode } from "@/hooks/use-auto-mode";
import {
useKeyboardShortcuts,
@@ -82,6 +83,7 @@ export function BoardView() {
description: "",
steps: [""],
images: [] as FeatureImage[],
skipTests: false,
});
const [isLoading, setIsLoading] = useState(true);
const [isMounted, setIsMounted] = useState(false);
@@ -90,6 +92,7 @@ export function BoardView() {
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
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
useEffect(() => {
@@ -164,11 +167,13 @@ export function BoardView() {
})
);
// Get unique categories from existing features for autocomplete suggestions
// Get unique categories from existing features AND persisted categories for autocomplete suggestions
const categorySuggestions = useMemo(() => {
const categories = features.map((f) => f.category).filter(Boolean);
return [...new Set(categories)].sort();
}, [features]);
const featureCategories = features.map((f) => f.category).filter(Boolean);
// Merge feature categories with persisted categories
const allCategories = [...featureCategories, ...persistedCategories];
return [...new Set(allCategories)].sort();
}, [features, persistedCategories]);
// Custom collision detection that prioritizes columns over cards
const collisionDetectionStrategy = useCallback((args: any) => {
@@ -217,6 +222,57 @@ export function BoardView() {
}
}, [currentProject, setFeatures]);
// Load persisted categories from file
const loadCategories = useCallback(async () => {
if (!currentProject) return;
try {
const api = getElectronAPI();
const result = await api.readFile(
`${currentProject.path}/.automaker/categories.json`
);
if (result.success && result.content) {
const parsed = JSON.parse(result.content);
if (Array.isArray(parsed)) {
setPersistedCategories(parsed);
}
}
} catch (error) {
console.error("Failed to load categories:", error);
// If file doesn't exist, that's fine - start with empty array
}
}, [currentProject]);
// Save a new category to the persisted categories file
const saveCategory = useCallback(async (category: string) => {
if (!currentProject || !category.trim()) return;
try {
const api = getElectronAPI();
// 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
// Write back to file
await api.writeFile(
`${currentProject.path}/.automaker/categories.json`,
JSON.stringify(categories, null, 2)
);
// Update state
setPersistedCategories(categories);
}
} catch (error) {
console.error("Failed to save category:", error);
}
}, [currentProject, persistedCategories]);
// Auto-show activity log when auto mode starts
useEffect(() => {
if (autoMode.isRunning && !showActivityLog) {
@@ -244,6 +300,11 @@ export function BoardView() {
loadFeatures();
}, [loadFeatures]);
// Load persisted categories on mount
useEffect(() => {
loadCategories();
}, [loadCategories]);
// Sync running tasks from electron backend on mount
useEffect(() => {
const syncRunningTasks = async () => {
@@ -392,14 +453,18 @@ export function BoardView() {
};
const handleAddFeature = () => {
const category = newFeature.category || "Uncategorized";
addFeature({
category: newFeature.category || "Uncategorized",
category,
description: newFeature.description,
steps: newFeature.steps.filter((s) => s.trim()),
status: "backlog",
images: newFeature.images,
skipTests: newFeature.skipTests,
});
setNewFeature({ category: "", description: "", steps: [""], images: [] });
// Persist the category
saveCategory(category);
setNewFeature({ category: "", description: "", steps: [""], images: [], skipTests: false });
setShowAddDialog(false);
};
@@ -410,7 +475,12 @@ export function BoardView() {
category: editingFeature.category,
description: editingFeature.description,
steps: editingFeature.steps,
skipTests: editingFeature.skipTests,
});
// Persist the category if it's new
if (editingFeature.category) {
saveCategory(editingFeature.category);
}
setEditingFeature(null);
};
@@ -421,29 +491,23 @@ export function BoardView() {
// Check if the feature is currently running
const isRunning = runningAutoTasks.includes(featureId);
const confirmMessage = isRunning
? "This feature has an agent running. Deleting it will stop the agent. Are you sure you want to delete this feature?"
: "Are you sure you want to delete this feature?";
if (window.confirm(confirmMessage)) {
// If the feature is running, stop the agent first
if (isRunning) {
try {
await autoMode.stopFeature(featureId);
toast.success("Agent stopped", {
description: `Stopped and deleted: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
});
} catch (error) {
console.error("[Board] Error stopping feature before delete:", error);
toast.error("Failed to stop agent", {
description: "The feature will still be deleted.",
});
}
// If the feature is running, stop the agent first
if (isRunning) {
try {
await autoMode.stopFeature(featureId);
toast.success("Agent stopped", {
description: `Stopped and deleted: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
});
} catch (error) {
console.error("[Board] Error stopping feature before delete:", error);
toast.error("Failed to stop agent", {
description: "The feature will still be deleted.",
});
}
// Remove the feature
removeFeature(featureId);
}
// Remove the feature immediately without confirmation
removeFeature(featureId);
};
const handleRunFeature = async (feature: Feature) => {
@@ -542,6 +606,24 @@ 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 });
moveFeature(feature.id, "verified");
toast.success("Feature verified", {
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() });
toast.info("Feature moved back", {
description: `Moved back to In Progress: ${feature.description.slice(0, 50)}${feature.description.length > 50 ? "..." : ""}`,
});
};
const checkContextExists = async (featureId: string): Promise<boolean> => {
if (!currentProject) return false;
@@ -828,6 +910,8 @@ export function BoardView() {
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}
@@ -867,7 +951,15 @@ export function BoardView() {
{/* Add Feature Dialog */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent data-testid="add-feature-dialog">
<DialogContent
data-testid="add-feature-dialog"
onKeyDown={(e) => {
if (e.shiftKey && e.key === "Enter" && newFeature.description) {
e.preventDefault();
handleAddFeature();
}
}}
>
<DialogHeader>
<DialogTitle>Add New Feature</DialogTitle>
<DialogDescription>
@@ -929,6 +1021,25 @@ export function BoardView() {
Add Step
</Button>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="skip-tests"
checked={newFeature.skipTests}
onCheckedChange={(checked) =>
setNewFeature({ ...newFeature, skipTests: checked === true })
}
data-testid="skip-tests-checkbox"
/>
<div className="flex items-center gap-2">
<Label htmlFor="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.
</p>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowAddDialog(false)}>
@@ -940,6 +1051,12 @@ export function BoardView() {
data-testid="confirm-add-feature"
>
Add Feature
<span
className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-white/10 border border-white/20"
data-testid="shortcut-confirm-add-feature"
>
</span>
</Button>
</DialogFooter>
</DialogContent>
@@ -974,8 +1091,9 @@ export function BoardView() {
</div>
<div className="space-y-2">
<Label htmlFor="edit-description">Description</Label>
<Input
<Textarea
id="edit-description"
placeholder="Describe the feature..."
value={editingFeature.description}
onChange={(e) =>
setEditingFeature({

View File

@@ -152,6 +152,10 @@ export const getElectronAPI = (): ElectronAPI => {
if (filePath.endsWith("feature_list.json")) {
return { success: true, content: JSON.stringify(mockFeatures, null, 2) };
}
if (filePath.endsWith("categories.json")) {
// Return empty array for categories when file doesn't exist yet
return { success: true, content: "[]" };
}
if (filePath.endsWith("app_spec.txt")) {
return {
success: true,