diff --git a/web/src/app/(app)/reviews/page.tsx b/web/src/app/(app)/reviews/page.tsx index 73b3166..f926ece 100644 --- a/web/src/app/(app)/reviews/page.tsx +++ b/web/src/app/(app)/reviews/page.tsx @@ -18,6 +18,15 @@ import { approveReviewItem, batchReviewItems, listAgentTimeline, listApprovalQue import { toast } from "@/lib/toast"; import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types"; +function isKeyboardShortcutSafe(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return true; + } + + const tagName = target.tagName.toLowerCase(); + return tagName !== "input" && tagName !== "textarea" && tagName !== "select" && !target.isContentEditable; +} + export default function ReviewsPage() { const [approvalQueue, setApprovalQueue] = useState([]); const [timeline, setTimeline] = useState([]); @@ -172,6 +181,88 @@ export default function ReviewsPage() { } } + useEffect(() => { + function selectRelativeQueueItem(offset: number) { + if (approvalQueue.length === 0) { + return; + } + + const currentIndex = Math.max( + 0, + approvalQueue.findIndex((item) => item.id === featuredProposal?.id), + ); + const nextIndex = Math.min(Math.max(currentIndex + offset, 0), approvalQueue.length - 1); + setSelectedApprovalId(approvalQueue[nextIndex]?.id ?? null); + } + + function handleKeyDown(event: KeyboardEvent) { + if ( + event.defaultPrevented || + event.metaKey || + event.ctrlKey || + event.altKey || + !isKeyboardShortcutSafe(event.target) + ) { + return; + } + + const key = event.key.toLowerCase(); + if (key === "arrowdown" || key === "j") { + event.preventDefault(); + selectRelativeQueueItem(1); + return; + } + + if (key === "arrowup" || key === "k") { + event.preventDefault(); + selectRelativeQueueItem(-1); + return; + } + + if (key === "x") { + event.preventDefault(); + selectAllForBatch(); + return; + } + + if ((key === "escape" || key === "c") && batchMode) { + event.preventDefault(); + clearBatch(); + return; + } + + if (key === "a") { + event.preventDefault(); + if (batchMode) { + void handleBatchDecision("approved"); + } else { + void handleDecision("approved"); + } + return; + } + + if (key === "r") { + event.preventDefault(); + if (batchMode) { + void handleBatchDecision("rejected"); + } else { + void handleDecision("rejected"); + } + } + } + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + approvalQueue, + batchMode, + clearBatch, + featuredProposal?.id, + handleBatchDecision, + handleDecision, + selectAllForBatch, + ]); + return (