From ffd8752cde92890336db1312a81860d6570b86ba Mon Sep 17 00:00:00 2001 From: SuperComboGamer Date: Sat, 13 Dec 2025 01:42:40 -0500 Subject: [PATCH] feat: add debounce to terminal shortcuts and show in keyboard layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 300ms cooldown to prevent rapid terminal creation when holding keys - Merge DEFAULT_KEYBOARD_SHORTCUTS with user shortcuts so terminal shortcuts (Alt+D, Alt+Shift+D, Alt+W) show in keyboard layout - Fix keyboard map to handle undefined shortcuts from old persisted state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/app/src/components/ui/keyboard-map.tsx | 37 +++++++++++++------ .../views/terminal-view/terminal-panel.tsx | 22 +++++++++-- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/apps/app/src/components/ui/keyboard-map.tsx b/apps/app/src/components/ui/keyboard-map.tsx index f880829c..4d9f1fe3 100644 --- a/apps/app/src/components/ui/keyboard-map.tsx +++ b/apps/app/src/components/ui/keyboard-map.tsx @@ -161,11 +161,18 @@ interface KeyboardMapProps { export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMapProps) { const { keyboardShortcuts } = useAppStore(); + // Merge with defaults to ensure new shortcuts are always shown + const mergedShortcuts = React.useMemo(() => ({ + ...DEFAULT_KEYBOARD_SHORTCUTS, + ...keyboardShortcuts, + }), [keyboardShortcuts]); + // Create a reverse map: base key -> list of shortcut names (including info about modifiers) const keyToShortcuts = React.useMemo(() => { const map: Record> = {}; - (Object.entries(keyboardShortcuts) as [keyof KeyboardShortcuts, string][]).forEach( + (Object.entries(mergedShortcuts) as [keyof KeyboardShortcuts, string][]).forEach( ([shortcutName, shortcutStr]) => { + if (!shortcutStr) return; // Skip undefined shortcuts const parsed = parseShortcut(shortcutStr); const normalizedKey = parsed.key.toUpperCase(); const hasModifiers = !!(parsed.shift || parsed.cmdCtrl || parsed.alt); @@ -176,7 +183,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap } ); return map; - }, [keyboardShortcuts]); + }, [mergedShortcuts]); const renderKey = (keyDef: { key: string; label: string; width: number }) => { const normalizedKey = keyDef.key.toUpperCase(); @@ -185,7 +192,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap const isBound = shortcuts.length > 0; const isSelected = selectedKey?.toUpperCase() === normalizedKey; const isModified = shortcuts.some( - (s) => keyboardShortcuts[s] !== DEFAULT_KEYBOARD_SHORTCUTS[s] + (s) => mergedShortcuts[s] !== DEFAULT_KEYBOARD_SHORTCUTS[s] ); // Get category for coloring (use first shortcut's category if multiple) @@ -250,7 +257,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap
{shortcuts.map((shortcut) => { - const shortcutStr = keyboardShortcuts[shortcut]; + const shortcutStr = mergedShortcuts[shortcut]; const displayShortcut = formatShortcut(shortcutStr, true); return (
@@ -266,7 +273,7 @@ export function KeyboardMap({ onKeySelect, selectedKey, className }: KeyboardMap {displayShortcut} - {keyboardShortcuts[shortcut] !== DEFAULT_KEYBOARD_SHORTCUTS[shortcut] && ( + {mergedShortcuts[shortcut] !== DEFAULT_KEYBOARD_SHORTCUTS[shortcut] && ( (custom) )}
@@ -353,6 +360,12 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa const [modifiers, setModifiers] = React.useState({ shift: false, cmdCtrl: false, alt: false }); const [shortcutError, setShortcutError] = React.useState(null); + // Merge with defaults to ensure new shortcuts are always shown + const mergedShortcuts = React.useMemo(() => ({ + ...DEFAULT_KEYBOARD_SHORTCUTS, + ...keyboardShortcuts, + }), [keyboardShortcuts]); + const groupedShortcuts = React.useMemo(() => { const groups: Record> = { navigation: [], @@ -365,13 +378,13 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa groups[category].push({ key: shortcut, label: SHORTCUT_LABELS[shortcut] ?? shortcut, - value: keyboardShortcuts[shortcut], + value: mergedShortcuts[shortcut], }); } ); return groups; - }, [keyboardShortcuts]); + }, [mergedShortcuts]); // Build the full shortcut string from key + modifiers const buildShortcutString = React.useCallback((key: string, mods: typeof modifiers) => { @@ -385,14 +398,14 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa // Check for conflicts with other shortcuts const checkConflict = React.useCallback((shortcutStr: string, currentKey: keyof KeyboardShortcuts) => { - const conflict = Object.entries(keyboardShortcuts).find( - ([k, v]) => k !== currentKey && v.toUpperCase() === shortcutStr.toUpperCase() + const conflict = Object.entries(mergedShortcuts).find( + ([k, v]) => k !== currentKey && v?.toUpperCase() === shortcutStr.toUpperCase() ); return conflict ? (SHORTCUT_LABELS[conflict[0] as keyof KeyboardShortcuts] ?? conflict[0]) : null; - }, [keyboardShortcuts]); + }, [mergedShortcuts]); const handleStartEdit = (key: keyof KeyboardShortcuts) => { - const currentValue = keyboardShortcuts[key]; + const currentValue = mergedShortcuts[key]; const parsed = parseShortcut(currentValue); setEditingShortcut(key); setKeyValue(parsed.key); @@ -495,7 +508,7 @@ export function ShortcutReferencePanel({ editable = false }: ShortcutReferencePa
{shortcuts.map(({ key, label, value }) => { - const isModified = keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key]; + const isModified = mergedShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key]; const isEditing = editingShortcut === key; return ( diff --git a/apps/app/src/components/views/terminal-view/terminal-panel.tsx b/apps/app/src/components/views/terminal-view/terminal-panel.tsx index aef9bd19..f0283e66 100644 --- a/apps/app/src/components/views/terminal-view/terminal-panel.tsx +++ b/apps/app/src/components/views/terminal-view/terminal-panel.tsx @@ -58,6 +58,7 @@ export function TerminalPanel({ const fitAddonRef = useRef(null); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); + const lastShortcutTimeRef = useRef(0); const [isTerminalReady, setIsTerminalReady] = useState(false); const [shellName, setShellName] = useState("shell"); @@ -182,28 +183,43 @@ export function TerminalPanel({ // Custom key handler to intercept terminal shortcuts // Return false to prevent xterm from handling the key + const SHORTCUT_COOLDOWN_MS = 300; // Prevent rapid firing + terminal.attachCustomKeyEventHandler((event) => { // Only intercept keydown events if (event.type !== 'keydown') return true; + // Check cooldown to prevent rapid terminal creation + const now = Date.now(); + const canTrigger = now - lastShortcutTimeRef.current > SHORTCUT_COOLDOWN_MS; + // Alt+D - Split right if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'd') { event.preventDefault(); - onSplitHorizontalRef.current(); + if (canTrigger) { + lastShortcutTimeRef.current = now; + onSplitHorizontalRef.current(); + } return false; } // Alt+Shift+D - Split down if (event.altKey && event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'd') { event.preventDefault(); - onSplitVerticalRef.current(); + if (canTrigger) { + lastShortcutTimeRef.current = now; + onSplitVerticalRef.current(); + } return false; } // Alt+W - Close terminal if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'w') { event.preventDefault(); - onCloseRef.current(); + if (canTrigger) { + lastShortcutTimeRef.current = now; + onCloseRef.current(); + } return false; }