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:
@@ -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">
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user