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:
Cody Seibert
2025-12-09 08:14:58 -05:00
parent 04b54bfadf
commit 9668f61878
2 changed files with 187 additions and 34 deletions

View File

@@ -67,7 +67,7 @@
"category": "Kanban",
"description": "For the first 10 in progress cards, add shortcut keys 1 through 0 on the keyboard for opening their output modal",
"steps": [],
"status": "in_progress"
"status": "verified"
},
{
"id": "feature-1765265001317-4eyqyif9z",

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback, useMemo } from "react";
import { useEffect, useState, useCallback, useMemo, useRef } from "react";
import {
DndContext,
DragEndEvent,
@@ -43,7 +43,7 @@ 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 } from "lucide-react";
import { Plus, RefreshCw, Play, StopCircle, Loader2, ChevronUp, ChevronDown, Users, Trash2, FastForward } from "lucide-react";
import { toast } from "sonner";
import { Slider } from "@/components/ui/slider";
import { useAutoMode } from "@/hooks/use-auto-mode";
@@ -89,6 +89,7 @@ export function BoardView() {
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);
// Make current project available globally for modal
useEffect(() => {
@@ -103,16 +104,50 @@ export function BoardView() {
// Auto mode hook
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
const boardShortcuts: KeyboardShortcut[] = useMemo(
() => [
{
key: ACTION_SHORTCUTS.addFeature,
action: () => setShowAddDialog(true),
description: "Add new feature",
},
],
[]
() => {
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}`,
});
});
return shortcuts;
},
[inProgressFeaturesForShortcuts]
);
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) {
return (
<div
@@ -665,15 +738,6 @@ export function BoardView() {
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={loadFeatures}
data-testid="refresh-board"
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
<Button
size="sm"
onClick={() => setShowAddDialog(true)}
@@ -715,25 +779,61 @@ export function BoardView() {
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}
>
{columnFeatures.map((feature) => (
<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)}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
/>
))}
{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)}
hasContext={featuresWithContext.has(feature.id)}
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
shortcutKey={shortcutKey}
/>
);
})}
</SortableContext>
</KanbanColumn>
);
@@ -937,6 +1037,59 @@ export function BoardView() {
featureDescription={outputFeature?.description || ""}
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>
);
}