mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
feat(kanban): Add keyboard shortcuts 1-9 and 0 for in-progress cards
- Added keyboard shortcuts for the first 10 in-progress cards - Keys 1-9 open the output modal for cards 1-9 - Key 0 opens the output modal for the 10th card - Shortcut key badges are displayed on in-progress cards - Shortcuts don't trigger when typing in inputs or dialogs Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,7 +67,7 @@
|
|||||||
"category": "Kanban",
|
"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": "For the first 10 in progress cards, add shortcut keys 1 through 0 on the keyboard for opening their output modal",
|
||||||
"steps": [],
|
"steps": [],
|
||||||
"status": "in_progress"
|
"status": "verified"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "feature-1765265001317-4eyqyif9z",
|
"id": "feature-1765265001317-4eyqyif9z",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
import { useEffect, useState, useCallback, useMemo, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -43,7 +43,7 @@ import { KanbanColumn } from "./kanban-column";
|
|||||||
import { KanbanCard } from "./kanban-card";
|
import { KanbanCard } from "./kanban-card";
|
||||||
import { AutoModeLog } from "./auto-mode-log";
|
import { AutoModeLog } from "./auto-mode-log";
|
||||||
import { AgentOutputModal } from "./agent-output-modal";
|
import { AgentOutputModal } from "./agent-output-modal";
|
||||||
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users } from "lucide-react";
|
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useAutoMode } from "@/hooks/use-auto-mode";
|
import { useAutoMode } from "@/hooks/use-auto-mode";
|
||||||
@@ -89,6 +89,7 @@ export function BoardView() {
|
|||||||
const [showOutputModal, setShowOutputModal] = useState(false);
|
const [showOutputModal, setShowOutputModal] = useState(false);
|
||||||
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
||||||
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(new Set());
|
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(new Set());
|
||||||
|
const [showDeleteAllVerifiedDialog, setShowDeleteAllVerifiedDialog] = useState(false);
|
||||||
|
|
||||||
// Make current project available globally for modal
|
// Make current project available globally for modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -103,16 +104,50 @@ export function BoardView() {
|
|||||||
// Auto mode hook
|
// Auto mode hook
|
||||||
const autoMode = useAutoMode();
|
const autoMode = useAutoMode();
|
||||||
|
|
||||||
|
// Get in-progress features for keyboard shortcuts (memoized for shortcuts)
|
||||||
|
const inProgressFeaturesForShortcuts = useMemo(() => {
|
||||||
|
return features.filter((f) => {
|
||||||
|
const isRunning = runningAutoTasks.includes(f.id);
|
||||||
|
return isRunning || f.status === "in_progress";
|
||||||
|
});
|
||||||
|
}, [features, runningAutoTasks]);
|
||||||
|
|
||||||
|
// Ref to hold the start next callback (to avoid dependency issues)
|
||||||
|
const startNextFeaturesRef = useRef<() => void>(() => {});
|
||||||
|
|
||||||
// Keyboard shortcuts for this view
|
// Keyboard shortcuts for this view
|
||||||
const boardShortcuts: KeyboardShortcut[] = useMemo(
|
const boardShortcuts: KeyboardShortcut[] = useMemo(
|
||||||
() => [
|
() => {
|
||||||
{
|
const shortcuts: KeyboardShortcut[] = [
|
||||||
key: ACTION_SHORTCUTS.addFeature,
|
{
|
||||||
action: () => setShowAddDialog(true),
|
key: ACTION_SHORTCUTS.addFeature,
|
||||||
description: "Add new feature",
|
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}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return shortcuts;
|
||||||
|
},
|
||||||
|
[inProgressFeaturesForShortcuts]
|
||||||
);
|
);
|
||||||
useKeyboardShortcuts(boardShortcuts);
|
useKeyboardShortcuts(boardShortcuts);
|
||||||
|
|
||||||
@@ -561,6 +596,44 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Start next features from backlog up to the concurrency limit
|
||||||
|
const handleStartNextFeatures = useCallback(async () => {
|
||||||
|
const backlogFeatures = features.filter((f) => f.status === "backlog");
|
||||||
|
const availableSlots = maxConcurrency - runningAutoTasks.length;
|
||||||
|
|
||||||
|
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.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backlogFeatures.length === 0) {
|
||||||
|
toast.info("No features in backlog", {
|
||||||
|
description: "Add features to the backlog first.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const featuresToStart = backlogFeatures.slice(0, availableSlots);
|
||||||
|
|
||||||
|
for (const feature of featuresToStart) {
|
||||||
|
// Update the feature status with startedAt timestamp
|
||||||
|
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(", "),
|
||||||
|
});
|
||||||
|
}, [features, maxConcurrency, runningAutoTasks.length, updateFeature]);
|
||||||
|
|
||||||
|
// Update ref when handleStartNextFeatures changes
|
||||||
|
useEffect(() => {
|
||||||
|
startNextFeaturesRef.current = handleStartNextFeatures;
|
||||||
|
}, [handleStartNextFeatures]);
|
||||||
|
|
||||||
if (!currentProject) {
|
if (!currentProject) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -665,15 +738,6 @@ export function BoardView() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={loadFeatures}
|
|
||||||
data-testid="refresh-board"
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setShowAddDialog(true)}
|
onClick={() => setShowAddDialog(true)}
|
||||||
@@ -715,25 +779,61 @@ export function BoardView() {
|
|||||||
color={column.color}
|
color={column.color}
|
||||||
count={columnFeatures.length}
|
count={columnFeatures.length}
|
||||||
isDoubleWidth={column.id === "in_progress"}
|
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
|
<SortableContext
|
||||||
items={columnFeatures.map((f) => f.id)}
|
items={columnFeatures.map((f) => f.id)}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
{columnFeatures.map((feature) => (
|
{columnFeatures.map((feature, index) => {
|
||||||
<KanbanCard
|
// Calculate shortcut key for in-progress cards (first 10 get 1-9, 0)
|
||||||
key={feature.id}
|
let shortcutKey: string | undefined;
|
||||||
feature={feature}
|
if (column.id === "in_progress" && index < 10) {
|
||||||
onEdit={() => setEditingFeature(feature)}
|
shortcutKey = index === 9 ? "0" : String(index + 1);
|
||||||
onDelete={() => handleDeleteFeature(feature.id)}
|
}
|
||||||
onViewOutput={() => handleViewOutput(feature)}
|
return (
|
||||||
onVerify={() => handleVerifyFeature(feature)}
|
<KanbanCard
|
||||||
onResume={() => handleResumeFeature(feature)}
|
key={feature.id}
|
||||||
onForceStop={() => handleForceStopFeature(feature)}
|
feature={feature}
|
||||||
hasContext={featuresWithContext.has(feature.id)}
|
onEdit={() => setEditingFeature(feature)}
|
||||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
onDelete={() => handleDeleteFeature(feature.id)}
|
||||||
/>
|
onViewOutput={() => handleViewOutput(feature)}
|
||||||
))}
|
onVerify={() => handleVerifyFeature(feature)}
|
||||||
|
onResume={() => handleResumeFeature(feature)}
|
||||||
|
onForceStop={() => handleForceStopFeature(feature)}
|
||||||
|
hasContext={featuresWithContext.has(feature.id)}
|
||||||
|
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||||
|
shortcutKey={shortcutKey}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</KanbanColumn>
|
</KanbanColumn>
|
||||||
);
|
);
|
||||||
@@ -937,6 +1037,59 @@ export function BoardView() {
|
|||||||
featureDescription={outputFeature?.description || ""}
|
featureDescription={outputFeature?.description || ""}
|
||||||
featureId={outputFeature?.id || ""}
|
featureId={outputFeature?.id || ""}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Delete All Verified Dialog */}
|
||||||
|
<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.
|
||||||
|
{getColumnFeatures("verified").length > 0 && (
|
||||||
|
<span className="block mt-2 text-yellow-500">
|
||||||
|
{getColumnFeatures("verified").length} feature(s) will be deleted.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={() => setShowDeleteAllVerifiedDialog(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
const verifiedFeatures = getColumnFeatures("verified");
|
||||||
|
for (const feature of verifiedFeatures) {
|
||||||
|
// Check if the feature is currently running
|
||||||
|
const isRunning = runningAutoTasks.includes(feature.id);
|
||||||
|
|
||||||
|
// If the feature is running, stop the agent first
|
||||||
|
if (isRunning) {
|
||||||
|
try {
|
||||||
|
await autoMode.stopFeature(feature.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Board] Error stopping feature before delete:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the feature
|
||||||
|
removeFeature(feature.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowDeleteAllVerifiedDialog(false);
|
||||||
|
toast.success("All verified features deleted", {
|
||||||
|
description: `Deleted ${verifiedFeatures.length} feature(s).`,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
data-testid="confirm-delete-all-verified"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
Delete All
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user