From 7834d3d2974d5d24cedbb6a54be693adfac2d40f Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:54:10 -0700 Subject: [PATCH] Redesign shortcuts settings UX (#4554) - Remove the Groups nav rail; filter shortcuts by search + status only - Fold keybindings.json path into the subsection subtitle and move the Edit File in Orca action into the header - Replace the floating hover-card action popover with an inline recorder: keycaps (or an Add shortcut placeholder) click to record in place, and Reset/Disable reveal on row hover/focus - Fix a grid align-content stretch bug that opened a large gap between shortcut groups in the scroll area Co-authored-by: Orca --- .../settings/KeybindingsFileActions.tsx | 124 ++++----- .../settings/ShortcutBindingRow.tsx | 244 +++++++++--------- .../settings/ShortcutFilterRail.tsx | 40 --- .../components/settings/ShortcutRowsList.tsx | 2 +- .../src/components/settings/ShortcutsPane.tsx | 115 ++++----- 5 files changed, 222 insertions(+), 303 deletions(-) diff --git a/src/renderer/src/components/settings/KeybindingsFileActions.tsx b/src/renderer/src/components/settings/KeybindingsFileActions.tsx index 4141abf97..77184f0a2 100644 --- a/src/renderer/src/components/settings/KeybindingsFileActions.tsx +++ b/src/renderer/src/components/settings/KeybindingsFileActions.tsx @@ -126,84 +126,54 @@ export function KeybindingsFileActions(): React.JSX.Element { return (
-
-
-
-

Keybindings JSON

-
-

- {keybindingSnapshot?.path ?? '~/.orca/keybindings.json'} -

-
-
-
- - - - - - - void openKeybindingsFile()}> - - Open with Default App - - void openKeybindingsInExternalEditor('code')}> - - Open in VS Code - - void openKeybindingsInExternalEditor('cursor')}> - - Open in Cursor - - - void revealKeybindingsFile()}> - - Reveal in File Manager - - void reloadKeybindings()}> - - Reload from Disk - - - -
-
-
- {keybindingSnapshot?.diagnostics.length ? ( -
- {keybindingSnapshot.diagnostics.map((diagnostic, index) => ( -

- {diagnostic.message} -

- ))} -
- ) : null} + + + + + + + void openKeybindingsFile()}> + + Open with Default App + + void openKeybindingsInExternalEditor('code')}> + + Open in VS Code + + void openKeybindingsInExternalEditor('cursor')}> + + Open in Cursor + + + void revealKeybindingsFile()}> + + Reveal in File Manager + + void reloadKeybindings()}> + + Reload from Disk + + +
) } diff --git a/src/renderer/src/components/settings/ShortcutBindingRow.tsx b/src/renderer/src/components/settings/ShortcutBindingRow.tsx index 5a2fda9a0..852edf92e 100644 --- a/src/renderer/src/components/settings/ShortcutBindingRow.tsx +++ b/src/renderer/src/components/settings/ShortcutBindingRow.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import { Ban, Keyboard, RotateCcw, Terminal } from 'lucide-react' +import { Ban, Plus, RotateCcw, Terminal } from 'lucide-react' import { formatKeybinding, type KeybindingActionId, @@ -10,7 +10,6 @@ import { cn } from '../../lib/utils' import { ShortcutKeyCombo } from '../ShortcutKeyCombo' import { Badge } from '../ui/badge' import { Button } from '../ui/button' -import { HoverCard, HoverCardContent, HoverCardTrigger } from '../ui/hover-card' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' import { SearchableSetting } from './SearchableSetting' @@ -37,29 +36,6 @@ export type ShortcutTerminalStatus = { description: string } -function BindingPreview({ - bindings, - platform -}: { - bindings: readonly string[] - platform: NodeJS.Platform -}): React.JSX.Element { - if (bindings.length === 0) { - return ( - - Unassigned - - ) - } - return ( - - {bindings.map((binding) => ( - - ))} - - ) -} - export function ShortcutBindingRow({ item, groupTitle, @@ -90,6 +66,7 @@ export function ShortcutBindingRow({ const statusMessage = error ?? (warnings.length > 0 ? warnings.join(' ') : '') const recordingMessage = recording ? 'Listening for shortcut. Esc cancels recording.' : '' const helperMessage = statusMessage || recordingMessage + const hasBinding = effective.length > 0 const handleRecordKeyDown = (event: React.KeyboardEvent): void => { if (!recording) { @@ -120,108 +97,91 @@ export function ShortcutBindingRow({ }) } + // Why: the recorder is the row's primary control — clicking the keys (or the + // "Add shortcut" placeholder) records a new binding in place, so the whole + // affordance lives inline rather than in a detached popover. + const recorderLabel = recording + ? `Press shortcut keys for ${item.title}. Escape cancels.` + : hasBinding + ? `Change shortcut for ${item.title}` + : `Add shortcut for ${item.title}` + return ( -
- {item.title} - {modified ? ( - - Modified - - ) : null} - {terminalStatus ? ( - - - - - {terminalStatus.label} - - - - {terminalStatus.description} - - - ) : null} -
- -
- {helperMessage ? {helperMessage} : null} -
- - - - - - -
+
+
+ {item.title} + {modified ? ( + + Modified + + ) : null} + {terminalStatus ? ( - + + {terminalStatus.label} + - {recording ? 'Listening for shortcut' : 'Change shortcut'} + {terminalStatus.description} + ) : null} +
+ {helperMessage ? ( + + {helperMessage} + + ) : null} +
+ +
+ {/* Reset/Disable reveal on hover or keyboard focus to keep the row calm; + they stay reachable via focus-within for keyboard users. */} + {hasBinding ? ( +
+ {modified ? ( + + + + + + Reset to default + + + ) : null} - Disable - - - - - - - - Reset + Disable shortcut
- - + ) : null} + + + + + + + {recording ? 'Listening for shortcut' : hasBinding ? 'Change shortcut' : 'Add shortcut'} + + +
) } diff --git a/src/renderer/src/components/settings/ShortcutFilterRail.tsx b/src/renderer/src/components/settings/ShortcutFilterRail.tsx index d60a8cf45..f588688d6 100644 --- a/src/renderer/src/components/settings/ShortcutFilterRail.tsx +++ b/src/renderer/src/components/settings/ShortcutFilterRail.tsx @@ -23,12 +23,6 @@ export type ShortcutRowsByGroup = { rows: ShortcutRowModel[] } -export type ShortcutGroupSummary = { - id: string - label: string - count: number -} - const SHORTCUT_FILTER_LABELS: Record = { all: 'All', modified: 'Modified', @@ -80,10 +74,7 @@ export function ShortcutFilterRail({ onQueryChange, filter, onFilterChange, - activeGroup, - onActiveGroupChange, filterCounts, - groupSummaries, visibleCount, totalCount }: { @@ -91,10 +82,7 @@ export function ShortcutFilterRail({ onQueryChange: (value: string) => void filter: ShortcutFilter onFilterChange: (value: ShortcutFilter) => void - activeGroup: string - onActiveGroupChange: (value: string) => void filterCounts: Record - groupSummaries: ShortcutGroupSummary[] visibleCount: number totalCount: number }): React.JSX.Element { @@ -162,34 +150,6 @@ export function ShortcutFilterRail({ ))}
- - ) } diff --git a/src/renderer/src/components/settings/ShortcutRowsList.tsx b/src/renderer/src/components/settings/ShortcutRowsList.tsx index a4ef16113..b26b81ae1 100644 --- a/src/renderer/src/components/settings/ShortcutRowsList.tsx +++ b/src/renderer/src/components/settings/ShortcutRowsList.tsx @@ -43,7 +43,7 @@ export function ShortcutRowsList({ } return ( -
+
{groups.map((group) => (

diff --git a/src/renderer/src/components/settings/ShortcutsPane.tsx b/src/renderer/src/components/settings/ShortcutsPane.tsx index a558b56e2..bfb51a6f6 100644 --- a/src/renderer/src/components/settings/ShortcutsPane.tsx +++ b/src/renderer/src/components/settings/ShortcutsPane.tsx @@ -26,7 +26,6 @@ import { matchesShortcutLocalSearch, ShortcutFilterRail, type ShortcutFilter, - type ShortcutGroupSummary, type ShortcutRowsByGroup } from './ShortcutFilterRail' import { ShortcutRowsList } from './ShortcutRowsList' @@ -137,7 +136,6 @@ export function ShortcutsPane(): React.JSX.Element { const [recordingActionId, setRecordingActionId] = useState(null) const [shortcutQuery, setShortcutQuery] = useState('') const [shortcutFilter, setShortcutFilter] = useState('all') - const [activeShortcutGroup, setActiveShortcutGroup] = useState('all') const groups = useMemo(groupDefinitions, []) const conflictByAction = useMemo(() => { @@ -192,29 +190,11 @@ export function ShortcutsPane(): React.JSX.Element { unassigned: baseVisibleRows.filter((row) => row.effective.length === 0).length, conflicts: baseVisibleRows.filter((row) => row.warnings.length > 0).length } - const groupSummaries: ShortcutGroupSummary[] = [ - { - id: 'all', - label: 'All shortcuts', - count: baseVisibleRows.filter((row) => matchesShortcutFilter(row, shortcutFilter)).length - }, - ...shortcutGroups.map((group) => ({ - id: group.title, - label: group.title, - count: group.rows.filter( - (row) => - matchesSettingsSearch(searchQuery, getShortcutSearchEntry(row)) && - matchesShortcutLocalSearch(row, shortcutSearchQuery, platform) && - matchesShortcutFilter(row, shortcutFilter) - ).length - })) - ] const visibleShortcutGroups = shortcutGroups .map((group) => ({ title: group.title, rows: group.rows.filter( (row) => - (activeShortcutGroup === 'all' || row.groupTitle === activeShortcutGroup) && matchesSettingsSearch(searchQuery, getShortcutSearchEntry(row)) && matchesShortcutLocalSearch(row, shortcutSearchQuery, platform) && matchesShortcutFilter(row, shortcutFilter) @@ -342,59 +322,78 @@ export function ShortcutsPane(): React.JSX.Element { return (
+ {showPolicy ? ( + + ) : null} + + Customize shortcuts visually or edit{' '} + + {keybindingSnapshot?.path ?? '~/.orca/keybindings.json'} + {' '} + directly. + + } + action={} /> + {keybindingSnapshot?.diagnostics.length ? ( +
+ {keybindingSnapshot.diagnostics.map((diagnostic, index) => ( +

+ {diagnostic.message} +

+ ))} +
+ ) : null} +
-
- {showPolicy ? ( - - ) : null} - - - - { - setRecordingActionId(actionId) - clearError(actionId) - }} - onCancelRecording={() => setRecordingActionId(null)} - onCapture={(actionId, input) => void captureBinding(actionId, input)} - onClearError={clearError} - onDisable={(actionId) => { - clearRecordingForAction(actionId) - void disableBinding(actionId) - }} - onReset={(actionId) => { - clearRecordingForAction(actionId) - void resetBinding(actionId) - }} - /> -
+ { + setRecordingActionId(actionId) + clearError(actionId) + }} + onCancelRecording={() => setRecordingActionId(null)} + onCapture={(actionId, input) => void captureBinding(actionId, input)} + onClearError={clearError} + onDisable={(actionId) => { + clearRecordingForAction(actionId) + void disableBinding(actionId) + }} + onReset={(actionId) => { + clearRecordingForAction(actionId) + void resetBinding(actionId) + }} + />