From d53969c198682fe83d5176c58e7badf1eb4566b8 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 1 Feb 2026 19:09:34 +0900 Subject: [PATCH 1/7] apply "none" for shortcuts --- .../desktop/src/components/shortcut-input.tsx | 89 +++++++++++++++---- apps/desktop/src/utils/shortcut-validation.ts | 5 ++ 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/components/shortcut-input.tsx b/apps/desktop/src/components/shortcut-input.tsx index e537567a..d029b732 100644 --- a/apps/desktop/src/components/shortcut-input.tsx +++ b/apps/desktop/src/components/shortcut-input.tsx @@ -1,10 +1,11 @@ import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Pencil, X } from "lucide-react"; +import { Button, buttonVariants } from "@/components/ui/button"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { X, Undo2 } from "lucide-react"; import { api } from "@/trpc/react"; import { toast } from "sonner"; import { getKeyFromKeycode } from "@/utils/keycode-map"; +import { cn } from "@/lib/utils"; interface ShortcutInputProps { value?: number[]; @@ -109,9 +110,11 @@ function RecordingDisplay({ function ShortcutDisplay({ value, onEdit, + onClear, }: { value?: number[]; onEdit: () => void; + onClear: () => void; }) { // Format array as display string (e.g., ["Fn", "Space"] -> "Fn+Space") const displayValue = value?.length @@ -119,7 +122,12 @@ function ShortcutDisplay({ : undefined; return ( - <> +
{displayValue && ( - + - +
); } @@ -147,9 +155,15 @@ export function ShortcutInput({ onRecordingShortcutChange, }: ShortcutInputProps) { const [activeKeys, setActiveKeys] = useState([]); + const [previousKeys, setPreviousKeys] = useState(() => { + const stored = localStorage.getItem("shortcuts"); + return stored ? JSON.parse(stored) : null; + }); const setRecordingStateMutation = api.settings.setShortcutRecordingState.useMutation(); + const hasShortcut = value && value.length > 0; + const handleStartRecording = () => { onRecordingShortcutChange(true); setRecordingStateMutation.mutate(true); @@ -161,6 +175,20 @@ export function ShortcutInput({ setRecordingStateMutation.mutate(false); }; + const handleClearRecording = () => { + if (value && value.length > 0) { + setPreviousKeys(value); + } + onChange([]); + }; + + const handleRestorePrevious = () => { + if (previousKeys && previousKeys.length > 0) { + onChange(previousKeys); + setPreviousKeys(null); + } + }; + // Subscribe to key events when recording // Note: activeKeys closure is fresh on each render because useSubscription // updates its callback reference, so previousKeys correctly captures the @@ -178,6 +206,7 @@ export function ShortcutInput({ if (result.valid && result.shortcut) { // Basic format is valid - let parent handle backend validation onChange(result.shortcut); + setPreviousKeys(null); } else { toast.error(result.error || "Invalid key combination"); } @@ -198,18 +227,46 @@ export function ShortcutInput({ } }, [isRecordingShortcut]); + // Sync previousValue to localStorage + useEffect(() => { + if (previousKeys) { + localStorage.setItem("shortcuts", JSON.stringify(previousKeys)); + } else { + localStorage.removeItem("shortcuts"); + } + }, [previousKeys]); + return ( -
- {isRecordingShortcut ? ( - - ) : ( - - )} -
+ {isRecordingShortcut ? ( + + ) : hasShortcut ? ( + + ) : ( +
+ + {previousKeys && previousKeys.length > 0 && ( + + )} +
+ )}
); } diff --git a/apps/desktop/src/utils/shortcut-validation.ts b/apps/desktop/src/utils/shortcut-validation.ts index be36e504..d59b9010 100644 --- a/apps/desktop/src/utils/shortcut-validation.ts +++ b/apps/desktop/src/utils/shortcut-validation.ts @@ -220,6 +220,11 @@ export function validateShortcutComprehensive( const { candidateShortcut, candidateType, shortcutsByType, platform } = context; + // Allow empty shortcut (user intentionally disabling) + if (candidateShortcut.length === 0) { + return { valid: true }; + } + const otherShortcuts = Object.entries(shortcutsByType) .filter(([shortcutType]) => shortcutType !== candidateType) .map(([, shortcutKeys]) => shortcutKeys); From 3388b88bf9e15981892a259426168d61d1d754fe Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 1 Feb 2026 20:08:48 +0900 Subject: [PATCH 2/7] fix loading "flash" --- .../desktop/src/components/shortcut-input.tsx | 95 ++++++++++++------- .../main/pages/settings/shortcuts/index.tsx | 10 +- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src/components/shortcut-input.tsx b/apps/desktop/src/components/shortcut-input.tsx index d029b732..28653a6e 100644 --- a/apps/desktop/src/components/shortcut-input.tsx +++ b/apps/desktop/src/components/shortcut-input.tsx @@ -148,6 +148,39 @@ function ShortcutDisplay({ ); } +function NoneDisplay({ + previousKeys, + onEdit, + onRestore, +}: { + previousKeys?: number[]; + onEdit: () => void; + onRestore: () => void; +}) { + return ( +
+ + {previousKeys && previousKeys.length > 0 && ( + + )} +
+ ); +} + export function ShortcutInput({ value, onChange, @@ -155,9 +188,9 @@ export function ShortcutInput({ onRecordingShortcutChange, }: ShortcutInputProps) { const [activeKeys, setActiveKeys] = useState([]); - const [previousKeys, setPreviousKeys] = useState(() => { + const [previousKeys, setPreviousKeys] = useState(() => { const stored = localStorage.getItem("shortcuts"); - return stored ? JSON.parse(stored) : null; + return stored ? JSON.parse(stored) : undefined; }); const setRecordingStateMutation = api.settings.setShortcutRecordingState.useMutation(); @@ -185,7 +218,7 @@ export function ShortcutInput({ const handleRestorePrevious = () => { if (previousKeys && previousKeys.length > 0) { onChange(previousKeys); - setPreviousKeys(null); + setPreviousKeys(undefined); } }; @@ -204,9 +237,8 @@ export function ShortcutInput({ const result = validateShortcutFormat(previousKeys); if (result.valid && result.shortcut) { - // Basic format is valid - let parent handle backend validation onChange(result.shortcut); - setPreviousKeys(null); + setPreviousKeys(undefined); } else { toast.error(result.error || "Invalid key combination"); } @@ -236,37 +268,32 @@ export function ShortcutInput({ } }, [previousKeys]); + if (value === undefined) { + return null; + } + return ( - {isRecordingShortcut ? ( - - ) : hasShortcut ? ( - - ) : ( -
- - {previousKeys && previousKeys.length > 0 && ( - - )} -
- )} +
+ {isRecordingShortcut ? ( + + ) : hasShortcut ? ( + + ) : ( + + )} +
); } diff --git a/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx b/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx index 4be71fe4..bb9fc3bb 100644 --- a/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx +++ b/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx @@ -7,12 +7,14 @@ import { api } from "@/trpc/react"; import { toast } from "sonner"; export function ShortcutsSettingsPage() { - const [pushToTalkShortcut, setPushToTalkShortcut] = useState([]); + const [pushToTalkShortcut, setPushToTalkShortcut] = useState< + number[] | undefined + >(); const [toggleRecordingShortcut, setToggleRecordingShortcut] = useState< - number[] - >([]); + number[] | undefined + >(); const [pasteLastTranscriptShortcut, setPasteLastTranscriptShortcut] = - useState([]); + useState(); const [recordingShortcut, setRecordingShortcut] = useState< "pushToTalk" | "toggleRecording" | "pasteLastTranscript" | null >(null); From b9a2f7a3a3c5d7054b4eec10d5e558745a649fb4 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 1 Feb 2026 20:20:29 +0900 Subject: [PATCH 3/7] extract hooks --- .../desktop/src/components/shortcut-input.tsx | 25 ++++-------- apps/desktop/src/hooks/useLocalStorage.ts | 21 ++++++++++ apps/desktop/src/hooks/usePreviousShortcut.ts | 38 +++++++++++++++++++ 3 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src/hooks/useLocalStorage.ts create mode 100644 apps/desktop/src/hooks/usePreviousShortcut.ts diff --git a/apps/desktop/src/components/shortcut-input.tsx b/apps/desktop/src/components/shortcut-input.tsx index 28653a6e..0ce35e6b 100644 --- a/apps/desktop/src/components/shortcut-input.tsx +++ b/apps/desktop/src/components/shortcut-input.tsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from "react"; import { Button, buttonVariants } from "@/components/ui/button"; +import { Undo2, X } from "lucide-react"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { X, Undo2 } from "lucide-react"; import { api } from "@/trpc/react"; import { toast } from "sonner"; import { getKeyFromKeycode } from "@/utils/keycode-map"; import { cn } from "@/lib/utils"; +import { usePreviousShortcut } from "@/hooks/usePreviousShortcut"; interface ShortcutInputProps { value?: number[]; @@ -188,10 +189,7 @@ export function ShortcutInput({ onRecordingShortcutChange, }: ShortcutInputProps) { const [activeKeys, setActiveKeys] = useState([]); - const [previousKeys, setPreviousKeys] = useState(() => { - const stored = localStorage.getItem("shortcuts"); - return stored ? JSON.parse(stored) : undefined; - }); + const { previousKeys, savePrevious, clearPrevious } = usePreviousShortcut(); const setRecordingStateMutation = api.settings.setShortcutRecordingState.useMutation(); @@ -210,15 +208,15 @@ export function ShortcutInput({ const handleClearRecording = () => { if (value && value.length > 0) { - setPreviousKeys(value); + savePrevious(value); } onChange([]); }; const handleRestorePrevious = () => { - if (previousKeys && previousKeys.length > 0) { + if (previousKeys.length > 0) { onChange(previousKeys); - setPreviousKeys(undefined); + clearPrevious(); } }; @@ -238,7 +236,7 @@ export function ShortcutInput({ if (result.valid && result.shortcut) { onChange(result.shortcut); - setPreviousKeys(undefined); + clearPrevious(); } else { toast.error(result.error || "Invalid key combination"); } @@ -259,15 +257,6 @@ export function ShortcutInput({ } }, [isRecordingShortcut]); - // Sync previousValue to localStorage - useEffect(() => { - if (previousKeys) { - localStorage.setItem("shortcuts", JSON.stringify(previousKeys)); - } else { - localStorage.removeItem("shortcuts"); - } - }, [previousKeys]); - if (value === undefined) { return null; } diff --git a/apps/desktop/src/hooks/useLocalStorage.ts b/apps/desktop/src/hooks/useLocalStorage.ts new file mode 100644 index 00000000..46e3a99d --- /dev/null +++ b/apps/desktop/src/hooks/useLocalStorage.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from "react"; + +export function useLocalStorage( + key: string, + initialValue: T, +): [T, React.Dispatch>] { + const [storedValue, setStoredValue] = useState(() => { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + }); + + useEffect(() => { + if (JSON.stringify(storedValue) === JSON.stringify(initialValue)) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, JSON.stringify(storedValue)); + } + }, [key, storedValue, initialValue]); + + return [storedValue, setStoredValue]; +} diff --git a/apps/desktop/src/hooks/usePreviousShortcut.ts b/apps/desktop/src/hooks/usePreviousShortcut.ts new file mode 100644 index 00000000..cafb2509 --- /dev/null +++ b/apps/desktop/src/hooks/usePreviousShortcut.ts @@ -0,0 +1,38 @@ +import { useCallback } from "react"; +import { useLocalStorage } from "./useLocalStorage"; + +const STORAGE_KEY = "previous-shortcut"; + +export function usePreviousShortcut() { + const [previousKeys, setPreviousKeys] = useLocalStorage( + STORAGE_KEY, + [], + ); + + const savePrevious = useCallback( + (keys: number[]) => { + if (keys.length > 0) { + setPreviousKeys(keys); + } + }, + [setPreviousKeys], + ); + + const restorePrevious = useCallback(() => { + const keys = previousKeys; + setPreviousKeys([]); + return keys; + }, [previousKeys, setPreviousKeys]); + + const clearPrevious = useCallback(() => { + setPreviousKeys([]); + }, [setPreviousKeys]); + + return { + previousKeys, + savePrevious, + restorePrevious, + clearPrevious, + hasPrevious: previousKeys.length > 0, + }; +} From f6b2d39c66e95f1706fe9ce84afd657cd8498a48 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 1 Feb 2026 20:33:13 +0900 Subject: [PATCH 4/7] address code rabbit comments --- .../desktop/src/components/shortcut-input.tsx | 11 ++++++---- apps/desktop/src/hooks/useLocalStorage.ts | 21 +++++++++++++------ apps/desktop/src/hooks/usePreviousShortcut.ts | 7 +++---- .../main/pages/settings/shortcuts/index.tsx | 3 +++ .../shared/OnboardingShortcutInput.tsx | 1 + 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/components/shortcut-input.tsx b/apps/desktop/src/components/shortcut-input.tsx index 0ce35e6b..686c9701 100644 --- a/apps/desktop/src/components/shortcut-input.tsx +++ b/apps/desktop/src/components/shortcut-input.tsx @@ -13,6 +13,7 @@ interface ShortcutInputProps { onChange: (value: number[]) => void; isRecordingShortcut?: boolean; onRecordingShortcutChange: (recording: boolean) => void; + shortcutId: string; } const MODIFIER_KEYS = new Set([ @@ -187,9 +188,11 @@ export function ShortcutInput({ onChange, isRecordingShortcut = false, onRecordingShortcutChange, + shortcutId, }: ShortcutInputProps) { const [activeKeys, setActiveKeys] = useState([]); - const { previousKeys, savePrevious, clearPrevious } = usePreviousShortcut(); + const { previousKeys, savePrevious, clearPrevious } = + usePreviousShortcut(shortcutId); const setRecordingStateMutation = api.settings.setShortcutRecordingState.useMutation(); @@ -227,12 +230,12 @@ export function ShortcutInput({ api.settings.activeKeysUpdates.useSubscription(undefined, { enabled: isRecordingShortcut, onData: (keys: number[]) => { - const previousKeys = activeKeys; + const prevActiveKeys = activeKeys; setActiveKeys(keys); // When any key is released, validate the combination - if (previousKeys.length > 0 && keys.length < previousKeys.length) { - const result = validateShortcutFormat(previousKeys); + if (prevActiveKeys.length > 0 && keys.length < prevActiveKeys.length) { + const result = validateShortcutFormat(prevActiveKeys); if (result.valid && result.shortcut) { onChange(result.shortcut); diff --git a/apps/desktop/src/hooks/useLocalStorage.ts b/apps/desktop/src/hooks/useLocalStorage.ts index 46e3a99d..c5cb66c0 100644 --- a/apps/desktop/src/hooks/useLocalStorage.ts +++ b/apps/desktop/src/hooks/useLocalStorage.ts @@ -5,15 +5,24 @@ export function useLocalStorage( initialValue: T, ): [T, React.Dispatch>] { const [storedValue, setStoredValue] = useState(() => { - const item = localStorage.getItem(key); - return item ? JSON.parse(item) : initialValue; + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch { + localStorage.removeItem(key); + return initialValue; + } }); useEffect(() => { - if (JSON.stringify(storedValue) === JSON.stringify(initialValue)) { - localStorage.removeItem(key); - } else { - localStorage.setItem(key, JSON.stringify(storedValue)); + try { + if (JSON.stringify(storedValue) === JSON.stringify(initialValue)) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, JSON.stringify(storedValue)); + } + } catch { + // Silently fail - localStorage may be unavailable or full } }, [key, storedValue, initialValue]); diff --git a/apps/desktop/src/hooks/usePreviousShortcut.ts b/apps/desktop/src/hooks/usePreviousShortcut.ts index cafb2509..e422269d 100644 --- a/apps/desktop/src/hooks/usePreviousShortcut.ts +++ b/apps/desktop/src/hooks/usePreviousShortcut.ts @@ -1,11 +1,10 @@ import { useCallback } from "react"; import { useLocalStorage } from "./useLocalStorage"; -const STORAGE_KEY = "previous-shortcut"; - -export function usePreviousShortcut() { +export function usePreviousShortcut(shortcutId: string) { + const storageKey = `previous-shortcut-${shortcutId}`; const [previousKeys, setPreviousKeys] = useLocalStorage( - STORAGE_KEY, + storageKey, [], ); diff --git a/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx b/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx index bb9fc3bb..ebe1d7bf 100644 --- a/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx +++ b/apps/desktop/src/renderer/main/pages/settings/shortcuts/index.tsx @@ -115,6 +115,7 @@ export function ShortcutsSettingsPage() { onRecordingShortcutChange={(recording) => setRecordingShortcut(recording ? "pushToTalk" : null) } + shortcutId="pushToTalk" /> @@ -142,6 +143,7 @@ export function ShortcutsSettingsPage() { onRecordingShortcutChange={(recording) => setRecordingShortcut(recording ? "toggleRecording" : null) } + shortcutId="toggleRecording" /> @@ -170,6 +172,7 @@ export function ShortcutsSettingsPage() { recording ? "pasteLastTranscript" : null, ) } + shortcutId="pasteLastTranscript" /> diff --git a/apps/desktop/src/renderer/onboarding/components/shared/OnboardingShortcutInput.tsx b/apps/desktop/src/renderer/onboarding/components/shared/OnboardingShortcutInput.tsx index f70e9d08..1e05b8ec 100644 --- a/apps/desktop/src/renderer/onboarding/components/shared/OnboardingShortcutInput.tsx +++ b/apps/desktop/src/renderer/onboarding/components/shared/OnboardingShortcutInput.tsx @@ -56,6 +56,7 @@ export function OnboardingShortcutInput() { onChange={handleShortcutChange} isRecordingShortcut={isRecording} onRecordingShortcutChange={setIsRecording} + shortcutId="pushToTalk" /> From 0e2be774db79712b3c5e7d0263bdc57207998b67 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 1 Feb 2026 20:57:10 +0900 Subject: [PATCH 5/7] fix ui drift --- apps/desktop/src/components/shortcut-input.tsx | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/components/shortcut-input.tsx b/apps/desktop/src/components/shortcut-input.tsx index 686c9701..56de9d97 100644 --- a/apps/desktop/src/components/shortcut-input.tsx +++ b/apps/desktop/src/components/shortcut-input.tsx @@ -1,11 +1,10 @@ import React, { useState, useEffect } from "react"; -import { Button, buttonVariants } from "@/components/ui/button"; +import { Button } from "@/components/ui/button"; import { Undo2, X } from "lucide-react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { api } from "@/trpc/react"; import { toast } from "sonner"; import { getKeyFromKeycode } from "@/utils/keycode-map"; -import { cn } from "@/lib/utils"; import { usePreviousShortcut } from "@/hooks/usePreviousShortcut"; interface ShortcutInputProps { @@ -124,12 +123,7 @@ function ShortcutDisplay({ : undefined; return ( -
+
{displayValue && ( void; }) { return ( -
+
From c92ec214c39799c348d416305c4b8dd6fe3a3969 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 1 Feb 2026 21:10:49 +0900 Subject: [PATCH 6/7] safe local storage access --- apps/desktop/src/hooks/useLocalStorage.ts | 50 ++++++++++++++++------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/hooks/useLocalStorage.ts b/apps/desktop/src/hooks/useLocalStorage.ts index c5cb66c0..47cf2b3b 100644 --- a/apps/desktop/src/hooks/useLocalStorage.ts +++ b/apps/desktop/src/hooks/useLocalStorage.ts @@ -1,30 +1,50 @@ import { useState, useEffect } from "react"; +// Safe localStorage utilities that never throw +export const safeStorage = { + getItem(key: string): string | null { + try { + return localStorage.getItem(key); + } catch { + return null; + } + }, + setItem(key: string, value: string): boolean { + try { + localStorage.setItem(key, value); + return true; + } catch { + return false; + } + }, + removeItem(key: string): boolean { + try { + localStorage.removeItem(key); + return true; + } catch { + return false; + } + }, +}; + export function useLocalStorage( key: string, initialValue: T, ): [T, React.Dispatch>] { - const [storedValue, setStoredValue] = useState(() => { + const [value, setValue] = useState(() => { + const item = safeStorage.getItem(key); + if (item === null) return initialValue; try { - const item = localStorage.getItem(key); - return item ? JSON.parse(item) : initialValue; + return JSON.parse(item); } catch { - localStorage.removeItem(key); + safeStorage.removeItem(key); return initialValue; } }); useEffect(() => { - try { - if (JSON.stringify(storedValue) === JSON.stringify(initialValue)) { - localStorage.removeItem(key); - } else { - localStorage.setItem(key, JSON.stringify(storedValue)); - } - } catch { - // Silently fail - localStorage may be unavailable or full - } - }, [key, storedValue, initialValue]); + safeStorage.setItem(key, JSON.stringify(value)); + }, [key, value]); - return [storedValue, setStoredValue]; + return [value, setValue]; } From 0e73ea4ef6fec1aa7cab6630142d07487c34ef64 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 1 Feb 2026 21:22:23 +0900 Subject: [PATCH 7/7] address coderabbit pr comments --- .../desktop/src/components/shortcut-input.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/components/shortcut-input.tsx b/apps/desktop/src/components/shortcut-input.tsx index 56de9d97..74ebc598 100644 --- a/apps/desktop/src/components/shortcut-input.tsx +++ b/apps/desktop/src/components/shortcut-input.tsx @@ -1,11 +1,11 @@ -import React, { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; import { Undo2, X } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { usePreviousShortcut } from "@/hooks/usePreviousShortcut"; import { api } from "@/trpc/react"; -import { toast } from "sonner"; import { getKeyFromKeycode } from "@/utils/keycode-map"; -import { usePreviousShortcut } from "@/hooks/usePreviousShortcut"; interface ShortcutInputProps { value?: number[]; @@ -101,6 +101,7 @@ function RecordingDisplay({ size="sm" className="h-6 w-6 p-0" onClick={onCancel} + aria-label="Cancel recording" > @@ -137,6 +138,7 @@ function ShortcutDisplay({ size="sm" className="h-6 w-6 p-0" onClick={onClear} + aria-label="Clear shortcut" > @@ -164,6 +166,7 @@ function NoneDisplay({ size="sm" className="h-6 w-6 p-0" onClick={onRestore} + aria-label="Restore previous shortcut" > @@ -180,6 +183,7 @@ export function ShortcutInput({ shortcutId, }: ShortcutInputProps) { const [activeKeys, setActiveKeys] = useState([]); + const activeKeysRef = useRef([]); const { previousKeys, savePrevious, clearPrevious } = usePreviousShortcut(shortcutId); const setRecordingStateMutation = @@ -212,14 +216,16 @@ export function ShortcutInput({ } }; + // Keep ref in sync with state for use in subscription callback + useEffect(() => { + activeKeysRef.current = activeKeys; + }, [activeKeys]); + // Subscribe to key events when recording - // Note: activeKeys closure is fresh on each render because useSubscription - // updates its callback reference, so previousKeys correctly captures the - // previous state value when onData fires. api.settings.activeKeysUpdates.useSubscription(undefined, { enabled: isRecordingShortcut, onData: (keys: number[]) => { - const prevActiveKeys = activeKeys; + const prevActiveKeys = activeKeysRef.current; setActiveKeys(keys); // When any key is released, validate the combination