Variant cycle keyboard shortcut (vibe-kanban) (#478)

* Commit changes from coding agent for task attempt faf9032c-6400-46f5-a1b8-7fa39dc79f0a

* Commit changes from coding agent for task attempt faf9032c-6400-46f5-a1b8-7fa39dc79f0a

* Commit changes from coding agent for task attempt faf9032c-6400-46f5-a1b8-7fa39dc79f0a

* Commit changes from coding agent for task attempt faf9032c-6400-46f5-a1b8-7fa39dc79f0a
This commit is contained in:
Alex Netsch
2025-08-15 09:33:22 +01:00
committed by GitHub
parent 0dbb991b73
commit 5c7f52bcfd
2 changed files with 66 additions and 3 deletions

View File

@@ -2,7 +2,7 @@ import { AlertCircle, Send, ChevronDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { FileSearchTextarea } from '@/components/ui/file-search-textarea';
import { useContext, useEffect, useMemo, useState } from 'react';
import { useContext, useEffect, useMemo, useState, useRef } from 'react';
import { attemptsApi } from '@/lib/api.ts';
import {
TaskAttemptDataContext,
@@ -17,6 +17,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';
import { useVariantCyclingShortcut } from '@/lib/keyboard-shortcuts';
export function TaskFollowUpSection() {
const { task, projectId } = useContext(TaskDetailsContext);
@@ -35,6 +37,8 @@ export function TaskFollowUpSection() {
const [selectedVariant, setSelectedVariant] = useState<string | null>(
defaultFollowUpVariant
);
const [isAnimating, setIsAnimating] = useState(false);
const variantButtonRef = useRef<HTMLButtonElement>(null);
// Get the profile from the selected attempt
const selectedProfile = selectedAttempt?.profile || null;
@@ -65,6 +69,14 @@ export function TaskFollowUpSection() {
setSelectedVariant(defaultFollowUpVariant);
}, [defaultFollowUpVariant]);
// Use the centralized keyboard shortcut hook for cycling through variants
useVariantCyclingShortcut({
currentProfile,
selectedVariant,
setSelectedVariant,
setIsAnimating,
});
const onSendFollowUp = async () => {
if (!task || !selectedAttempt || !followUpMessage.trim()) return;
@@ -134,9 +146,13 @@ export function TaskFollowUpSection() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
ref={variantButtonRef}
variant="outline"
size="sm"
className="h-10 w-24 px-2 flex items-center justify-between"
className={cn(
'h-10 w-24 px-2 flex items-center justify-between transition-all',
isAnimating && 'scale-105 bg-accent'
)}
>
<span className="text-xs truncate flex-1 text-left">
{selectedVariant || 'Default'}
@@ -171,9 +187,10 @@ export function TaskFollowUpSection() {
// Show disabled button when profile exists but has no variants
return (
<Button
ref={variantButtonRef}
variant="outline"
size="sm"
className="h-10 w-24 px-2 flex items-center justify-between"
className="h-10 w-24 px-2 flex items-center justify-between transition-all"
disabled
>
<span className="text-xs truncate flex-1 text-left">

View File

@@ -1,5 +1,6 @@
import { useCallback, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import type { ProfileConfig } from 'shared/types';
// Define available keyboard shortcuts
export interface KeyboardShortcut {
@@ -264,3 +265,48 @@ export function useKanbanKeyboardNavigation({
preserveIndexOnColumnSwitch,
]);
}
// Hook for cycling through profile variants with Left Shift + Tab
export function useVariantCyclingShortcut({
currentProfile,
selectedVariant,
setSelectedVariant,
setIsAnimating,
}: {
currentProfile: ProfileConfig | null | undefined;
selectedVariant: string | null;
setSelectedVariant: (variant: string | null) => void;
setIsAnimating: (animating: boolean) => void;
}) {
useEffect(() => {
if (!currentProfile?.variants || currentProfile.variants.length === 0) {
return;
}
const handleKeyDown = (e: KeyboardEvent) => {
// Check for Left Shift + Tab
if (e.shiftKey && e.key === 'Tab') {
e.preventDefault();
// Build the variant cycle: null (Default) → variant1 → variant2 → ... → null
const variants = currentProfile.variants;
const variantLabels = variants.map((v) => v.label);
const allOptions = [null, ...variantLabels];
// Find current index and cycle to next
const currentIndex = allOptions.findIndex((v) => v === selectedVariant);
const nextIndex = (currentIndex + 1) % allOptions.length;
const nextVariant = allOptions[nextIndex];
setSelectedVariant(nextVariant);
// Trigger animation
setIsAnimating(true);
setTimeout(() => setIsAnimating(false), 300);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [currentProfile, selectedVariant, setSelectedVariant, setIsAnimating]);
}