From 6744f19a497a8eb4beffc9a5bc73d604fc7b817a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 25 May 2026 11:36:53 -0700 Subject: [PATCH] Refine shortcut settings UX (#2787) * Compact shortcut row actions * Redesign shortcuts settings controls * Refine shortcuts settings UX * Polish shortcut settings interactions --- .../src/components/settings/GeneralPane.tsx | 16 ++ .../settings/RecentTabOrderControl.tsx | 44 +++ .../src/components/settings/Settings.tsx | 50 +++- .../components/settings/SettingsSection.tsx | 9 +- .../settings/ShortcutBindingRow.tsx | 222 ++++++++------- .../settings/ShortcutFilterRail.tsx | 195 +++++++++++++ .../components/settings/ShortcutRowsList.tsx | 78 +++++ .../ShortcutTerminalPolicyControl.tsx | 50 ++++ .../src/components/settings/ShortcutsPane.tsx | 267 +++++++++--------- .../src/components/settings/general-search.ts | 18 ++ .../components/settings/shortcuts-search.ts | 9 +- src/renderer/src/hooks/useComposerState.ts | 31 -- src/shared/keybindings.ts | 9 - 13 files changed, 706 insertions(+), 292 deletions(-) create mode 100644 src/renderer/src/components/settings/RecentTabOrderControl.tsx create mode 100644 src/renderer/src/components/settings/ShortcutFilterRail.tsx create mode 100644 src/renderer/src/components/settings/ShortcutRowsList.tsx create mode 100644 src/renderer/src/components/settings/ShortcutTerminalPolicyControl.tsx diff --git a/src/renderer/src/components/settings/GeneralPane.tsx b/src/renderer/src/components/settings/GeneralPane.tsx index 822016e2f79..d16a2b3f9dc 100644 --- a/src/renderer/src/components/settings/GeneralPane.tsx +++ b/src/renderer/src/components/settings/GeneralPane.tsx @@ -22,12 +22,14 @@ import { GENERAL_CACHE_TIMER_SEARCH_ENTRIES, GENERAL_CLI_SEARCH_ENTRIES, GENERAL_EDITOR_SEARCH_ENTRIES, + GENERAL_NAVIGATION_SEARCH_ENTRIES, GENERAL_PANE_SEARCH_ENTRIES, GENERAL_SUPPORT_SEARCH_ENTRIES, GENERAL_UPDATE_SEARCH_ENTRIES, GENERAL_WORKSPACE_SEARCH_ENTRIES } from './general-search' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' +import { RecentTabOrderControl } from './RecentTabOrderControl' import { SearchableSetting } from './SearchableSetting' import { matchesSettingsSearch } from './settings-search' import { @@ -220,6 +222,20 @@ export function GeneralPane({ settings, updateSettings }: GeneralPaneProps): Rea } const visibleSections = [ + matchesSettingsSearch(searchQuery, GENERAL_NAVIGATION_SEARCH_ENTRIES) ? ( +
+ + [ + entry.title, + entry.description ?? '', + ...(entry.keywords ?? []) + ])} + updateSettings={updateSettings} + /> +
+ ) : null, matchesSettingsSearch(searchQuery, GENERAL_WORKSPACE_SEARCH_ENTRIES) ? (
Promise | void +}): React.JSX.Element { + return ( + + + void updateSettings({ ctrlTabOrderMode: value as CtrlTabOrderMode }) + } + > + + + + + Most recent + Tab strip order + + + } + /> + + ) +} diff --git a/src/renderer/src/components/settings/Settings.tsx b/src/renderer/src/components/settings/Settings.tsx index 3e76fcddad0..47488fefb04 100644 --- a/src/renderer/src/components/settings/Settings.tsx +++ b/src/renderer/src/components/settings/Settings.tsx @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { toast } from 'sonner' import { Info } from 'lucide-react' import type { OrcaHooks } from '../../../../shared/types' import { isFolderRepo } from '../../../../shared/repo-kind' @@ -43,6 +44,7 @@ import { PrivacyPane } from './PrivacyPane' import { SettingsSidebar } from './SettingsSidebar' import { ActiveSettingsSectionProvider, SettingsSection } from './SettingsSection' import { matchesSettingsSearch } from './settings-search' +import { cn } from '@/lib/utils' import { checkRuntimeHooks } from '@/runtime/runtime-hooks-client' import { useWindowsTerminalCapabilities } from '@/lib/windows-terminal-capabilities' import { getShortcutPlatform } from '@/lib/shortcut-platform' @@ -73,6 +75,9 @@ const SETTINGS_NAV_GROUPS = [ { id: 'experimental', title: 'Experimental' } ] as const +const SHORTCUTS_ESCAPE_CONFIRM_TOAST_ID = 'shortcuts-escape-confirm' +const SHORTCUTS_ESCAPE_CONFIRM_WINDOW_MS = 2200 + function getSettingsSectionId(pane: SettingsNavTarget, repoId: string | null): string { if (pane === 'repo' && repoId) { return `repo-${repoId}` @@ -195,6 +200,7 @@ function Settings(): React.JSX.Element { const pendingScrollTargetRef = useRef(null) const repoHooksRequestSeqRef = useRef(0) const repoHooksRuntimeIdentityRef = useRef('local') + const shortcutsEscapeConfirmUntilRef = useRef(0) const confirmDiscardCommitPromptChanges = useCallback(async (): Promise => { if (!hasUnsavedCommitPromptChanges) { @@ -263,12 +269,29 @@ function Settings(): React.JSX.Element { if (isEditableTarget(event.target)) { return } + if (activeSectionId === 'shortcuts') { + event.preventDefault() + const now = Date.now() + if (now <= shortcutsEscapeConfirmUntilRef.current) { + shortcutsEscapeConfirmUntilRef.current = 0 + toast.dismiss(SHORTCUTS_ESCAPE_CONFIRM_TOAST_ID) + void closeSettingsPageWithPromptGuard() + return + } + shortcutsEscapeConfirmUntilRef.current = now + SHORTCUTS_ESCAPE_CONFIRM_WINDOW_MS + toast.info('Press ESC again to exit settings', { + id: SHORTCUTS_ESCAPE_CONFIRM_TOAST_ID, + duration: SHORTCUTS_ESCAPE_CONFIRM_WINDOW_MS, + className: 'whitespace-nowrap' + }) + return + } void closeSettingsPageWithPromptGuard() } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) - }, [closeSettingsPageWithPromptGuard]) + }, [activeSectionId, closeSettingsPageWithPromptGuard]) useEffect(() => { const handleBeforeUnload = (event: BeforeUnloadEvent): void => { @@ -655,6 +678,8 @@ function Settings(): React.JSX.Element { return { ...section, badgeColor: repo?.badgeColor, isRemote: !!repo?.connectionId } }) const isSectionMounted = (sectionId: string): boolean => neededSectionIds.has(sectionId) + const isFocusedShortcutsPane = + activeSectionId === 'shortcuts' && settingsSearchQuery.trim() === '' return (
@@ -671,8 +696,19 @@ function Settings(): React.JSX.Element { />
-
-
+
+
{visibleNavSections.length === 0 ? (
No settings found for "{settingsSearchQuery.trim()}" @@ -877,6 +913,14 @@ function Settings(): React.JSX.Element { title="Shortcuts" description="Keyboard shortcuts for common actions." searchEntries={getSectionSearchEntries('shortcuts')} + className={ + isFocusedShortcutsPane + ? 'flex min-h-0 flex-1 flex-col space-y-0 gap-6' + : undefined + } + bodyClassName={ + isFocusedShortcutsPane ? 'min-h-0 flex-1 overflow-hidden' : undefined + } > {isSectionMounted('shortcuts') ? : null} diff --git a/src/renderer/src/components/settings/SettingsSection.tsx b/src/renderer/src/components/settings/SettingsSection.tsx index eb36e707b07..4ee84cbdc60 100644 --- a/src/renderer/src/components/settings/SettingsSection.tsx +++ b/src/renderer/src/components/settings/SettingsSection.tsx @@ -18,6 +18,7 @@ type SettingsSectionProps = { searchEntries?: SettingsSearchEntry[] children?: React.ReactNode className?: string + bodyClassName?: string badge?: string badgeAccessory?: React.ReactNode forceVisible?: boolean @@ -39,6 +40,7 @@ export function SettingsSection({ searchEntries, children, className, + bodyClassName, badge, badgeAccessory, forceVisible = false, @@ -80,7 +82,12 @@ export function SettingsSection({ {/* Why: body content sits in a visually distinct band — a soft card with rounded corners and tight inner padding — so each row group reads as contained inside the section, not as a continuation of the sidebar. */} -
+
{children}
diff --git a/src/renderer/src/components/settings/ShortcutBindingRow.tsx b/src/renderer/src/components/settings/ShortcutBindingRow.tsx index a2f621bd6e7..5a2fda9a0d0 100644 --- a/src/renderer/src/components/settings/ShortcutBindingRow.tsx +++ b/src/renderer/src/components/settings/ShortcutBindingRow.tsx @@ -10,6 +10,7 @@ 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' @@ -45,17 +46,17 @@ function BindingPreview({ }): React.JSX.Element { if (bindings.length === 0) { return ( -
+ Unassigned -
+ ) } return ( -
+ {bindings.map((binding) => ( ))} -
+ ) } @@ -124,109 +125,132 @@ export function ShortcutBindingRow({ title={item.title} description={`${groupTitle} shortcut`} keywords={[...item.searchKeywords]} - className="grid min-h-[54px] grid-cols-1 gap-x-3 rounded-md px-2 py-1.5 transition-colors hover:bg-accent/40 lg:grid-cols-[minmax(0,1.1fr)_minmax(10rem,0.8fr)_10rem_4rem] lg:items-center" + className="group relative grid min-h-[54px] max-w-none grid-cols-1 gap-x-3 rounded-md px-2 py-1.5 transition-colors hover:bg-accent/40 lg:grid-cols-[minmax(0,1fr)_minmax(12rem,auto)] lg:grid-rows-[minmax(1.75rem,auto)_1rem] lg:items-start" > -
-
- {item.title} - {modified ? ( - - Modified - - ) : null} - {terminalStatus ? ( +
+ {item.title} + {modified ? ( + + Modified + + ) : null} + {terminalStatus ? ( + + + + + {terminalStatus.label} + + + + {terminalStatus.description} + + + ) : null} +
+ +
+ {helperMessage ? {helperMessage} : null} +
+ + + + + + +
- { + if (recording) { + return + } + onStartRecording(item.id) + }} + onKeyDown={handleRecordKeyDown} + className={cn( + 'text-muted-foreground hover:text-foreground', + recording && + 'border border-ring bg-accent text-accent-foreground ring-[3px] ring-ring/30' + )} > - - {terminalStatus.label} - + + - {terminalStatus.description} + {recording ? 'Listening for shortcut' : 'Change shortcut'} - ) : null} -
-
- {helperMessage ? {helperMessage} : null} -
-
- -
- -
- - - -
- - - - - - Disable - - - - - - - - Reset - - -
+ + + + + + Disable + + + + + + + + Reset + + +
+ + ) } diff --git a/src/renderer/src/components/settings/ShortcutFilterRail.tsx b/src/renderer/src/components/settings/ShortcutFilterRail.tsx new file mode 100644 index 00000000000..d60a8cf457a --- /dev/null +++ b/src/renderer/src/components/settings/ShortcutFilterRail.tsx @@ -0,0 +1,195 @@ +import React from 'react' +import { Search, X } from 'lucide-react' +import { formatKeybindingList, type KeybindingDefinition } from '../../../../shared/keybindings' +import { cn } from '../../lib/utils' +import { Button } from '../ui/button' +import { Input } from '../ui/input' +import type { ShortcutTerminalStatus } from './ShortcutBindingRow' +import type { SettingsSearchEntry } from './settings-search' + +export type ShortcutFilter = 'all' | 'modified' | 'unassigned' | 'conflicts' + +export type ShortcutRowModel = { + item: KeybindingDefinition + groupTitle: string + effective: readonly string[] + modified: boolean + warnings: readonly string[] + terminalStatus?: ShortcutTerminalStatus +} + +export type ShortcutRowsByGroup = { + title: string + rows: ShortcutRowModel[] +} + +export type ShortcutGroupSummary = { + id: string + label: string + count: number +} + +const SHORTCUT_FILTER_LABELS: Record = { + all: 'All', + modified: 'Modified', + unassigned: 'Unassigned', + conflicts: 'Conflicts' +} + +export function getShortcutSearchEntry(row: ShortcutRowModel): SettingsSearchEntry { + return { + title: row.item.title, + description: `${row.groupTitle} shortcut`, + keywords: [...row.item.searchKeywords] + } +} + +export function matchesShortcutFilter(row: ShortcutRowModel, filter: ShortcutFilter): boolean { + switch (filter) { + case 'modified': + return row.modified + case 'unassigned': + return row.effective.length === 0 + case 'conflicts': + return row.warnings.length > 0 + case 'all': + return true + } +} + +export function matchesShortcutLocalSearch( + row: ShortcutRowModel, + query: string, + platform: NodeJS.Platform +): boolean { + if (!query) { + return true + } + const searchableText = [ + row.item.title, + row.item.id, + row.groupTitle, + ...row.item.searchKeywords, + formatKeybindingList(row.effective, platform) + ] + return searchableText.some((value) => value.toLowerCase().includes(query)) +} + +export function ShortcutFilterRail({ + query, + onQueryChange, + filter, + onFilterChange, + activeGroup, + onActiveGroupChange, + filterCounts, + groupSummaries, + visibleCount, + totalCount +}: { + query: string + 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 { + const filters = (Object.keys(SHORTCUT_FILTER_LABELS) as ShortcutFilter[]).map((id) => ({ + id, + label: SHORTCUT_FILTER_LABELS[id], + count: filterCounts[id] + })) + + return ( + + ) +} diff --git a/src/renderer/src/components/settings/ShortcutRowsList.tsx b/src/renderer/src/components/settings/ShortcutRowsList.tsx new file mode 100644 index 00000000000..a4ef161134a --- /dev/null +++ b/src/renderer/src/components/settings/ShortcutRowsList.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import type { KeybindingActionId, KeybindingInput } from '../../../../shared/keybindings' +import { cn } from '../../lib/utils' +import { ShortcutBindingRow } from './ShortcutBindingRow' +import type { ShortcutRowsByGroup } from './ShortcutFilterRail' + +export function ShortcutRowsList({ + className, + groups, + platform, + errors, + recordingActionId, + onStartRecording, + onCancelRecording, + onCapture, + onClearError, + onDisable, + onReset +}: { + className?: string + groups: ShortcutRowsByGroup[] + platform: NodeJS.Platform + errors: Partial> + recordingActionId: KeybindingActionId | null + onStartRecording: (actionId: KeybindingActionId) => void + onCancelRecording: () => void + onCapture: (actionId: KeybindingActionId, input: KeybindingInput) => void + onClearError: (actionId: KeybindingActionId) => void + onDisable: (actionId: KeybindingActionId) => void + onReset: (actionId: KeybindingActionId) => void +}): React.JSX.Element { + if (groups.length === 0) { + return ( +
+ No shortcuts match those filters. +
+ ) + } + + return ( +
+ {groups.map((group) => ( +
+

+ {group.title} +

+
+ {group.rows.map((row) => ( + + ))} +
+
+ ))} +
+ ) +} diff --git a/src/renderer/src/components/settings/ShortcutTerminalPolicyControl.tsx b/src/renderer/src/components/settings/ShortcutTerminalPolicyControl.tsx new file mode 100644 index 00000000000..338bab5ff25 --- /dev/null +++ b/src/renderer/src/components/settings/ShortcutTerminalPolicyControl.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import type { TerminalShortcutPolicy } from '../../../../shared/keybindings' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' +import { SearchableSetting } from './SearchableSetting' +import { SettingsRow } from './SettingsFormControls' + +export function ShortcutTerminalPolicyControl({ + terminalShortcutPolicy, + keywords, + updateSettings +}: { + terminalShortcutPolicy: TerminalShortcutPolicy + keywords?: string[] + updateSettings: (updates: { + terminalShortcutPolicy?: TerminalShortcutPolicy + }) => Promise | void +}): React.JSX.Element { + return ( + + + void updateSettings({ + terminalShortcutPolicy: value as TerminalShortcutPolicy + }) + } + > + + + + + Orca first + Terminal first + + + } + /> + + ) +} diff --git a/src/renderer/src/components/settings/ShortcutsPane.tsx b/src/renderer/src/components/settings/ShortcutsPane.tsx index af0d3452779..1eb02e92107 100644 --- a/src/renderer/src/components/settings/ShortcutsPane.tsx +++ b/src/renderer/src/components/settings/ShortcutsPane.tsx @@ -1,5 +1,4 @@ import React, { useMemo, useState } from 'react' -import type { CtrlTabOrderMode } from '../../../../shared/types' import { KEYBINDING_DEFINITIONS, findKeybindingConflicts, @@ -18,18 +17,22 @@ import { type TerminalShortcutPolicy } from '../../../../shared/keybindings' import { useAppStore } from '../../store' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { KeybindingsFileActions } from './KeybindingsFileActions' -import { SearchableSetting } from './SearchableSetting' -import { SettingsRow, SettingsSubsectionHeader } from './SettingsFormControls' -import { ShortcutBindingRow, type ShortcutTerminalStatus } from './ShortcutBindingRow' -import { matchesSettingsSearch, type SettingsSearchEntry } from './settings-search' +import { SettingsSubsectionHeader } from './SettingsFormControls' +import type { ShortcutTerminalStatus } from './ShortcutBindingRow' import { - CTRL_TAB_BEHAVIOR_SEARCH_ENTRY, - SHORTCUTS_PANE_SEARCH_ENTRIES, - TERMINAL_SHORTCUT_POLICY_SEARCH_ENTRY -} from './shortcuts-search' -export { SHORTCUTS_PANE_SEARCH_ENTRIES } + getShortcutSearchEntry, + matchesShortcutFilter, + matchesShortcutLocalSearch, + ShortcutFilterRail, + type ShortcutFilter, + type ShortcutGroupSummary, + type ShortcutRowsByGroup +} from './ShortcutFilterRail' +import { ShortcutRowsList } from './ShortcutRowsList' +import { ShortcutTerminalPolicyControl } from './ShortcutTerminalPolicyControl' +import { TERMINAL_SHORTCUT_POLICY_SEARCH_ENTRY } from './shortcuts-search' +import { matchesSettingsSearch, normalizeSettingsSearchQuery } from './settings-search' type ShortcutGroup = { title: string @@ -118,7 +121,6 @@ function getShortcutTerminalStatus( export function ShortcutsPane(): React.JSX.Element { const searchQuery = useAppStore((state) => state.settingsSearchQuery) - const ctrlTabOrderMode = useAppStore((state) => state.settings?.ctrlTabOrderMode ?? 'mru') const terminalShortcutPolicy = useAppStore( (state) => state.settings?.terminalShortcutPolicy ?? 'orca-first' ) @@ -130,22 +132,11 @@ export function ShortcutsPane(): React.JSX.Element { const disableKeybindingAction = useAppStore((state) => state.disableKeybindingAction) const [errors, setErrors] = useState>>({}) const [recordingActionId, setRecordingActionId] = useState(null) + const [shortcutQuery, setShortcutQuery] = useState('') + const [shortcutFilter, setShortcutFilter] = useState('all') + const [activeShortcutGroup, setActiveShortcutGroup] = useState('all') const groups = useMemo(groupDefinitions, []) - const groupEntries = useMemo>( - () => - Object.fromEntries( - groups.map((group) => [ - group.title, - group.items.map((item) => ({ - title: item.title, - description: `${group.title} shortcut`, - keywords: [...item.searchKeywords] - })) - ]) - ), - [groups] - ) const conflictByAction = useMemo(() => { const result = new Map() for (const conflict of findKeybindingConflicts(platform, keybindings)) { @@ -161,6 +152,76 @@ export function ShortcutsPane(): React.JSX.Element { } return result }, [keybindings]) + const shortcutGroups = useMemo( + () => + groups.map((group) => ({ + title: group.title, + rows: group.items.map((item) => { + const effective = getEffectiveKeybindingsForAction(item.id, platform, keybindings) + const modified = hasOwnBindingOverride(keybindings, item.id) + const warnings = conflictByAction.get(item.id) ?? [] + return { + item, + groupTitle: group.title, + effective, + modified, + warnings, + terminalStatus: getShortcutTerminalStatus( + item, + terminalShortcutPolicy, + effective.length > 0 + ) + } + }) + })), + [conflictByAction, groups, keybindings, terminalShortcutPolicy] + ) + const shortcutSearchQuery = normalizeSettingsSearchQuery(shortcutQuery) + const shortcutRows = shortcutGroups.flatMap((group) => group.rows) + const baseVisibleRows = shortcutRows.filter( + (row) => + matchesSettingsSearch(searchQuery, getShortcutSearchEntry(row)) && + matchesShortcutLocalSearch(row, shortcutSearchQuery, platform) + ) + const filterCounts: Record = { + all: baseVisibleRows.length, + modified: baseVisibleRows.filter((row) => row.modified).length, + 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) + ) + })) + .filter((group) => group.rows.length > 0) + const visibleShortcutCount = visibleShortcutGroups.reduce( + (sum, group) => sum + group.rows.length, + 0 + ) const saveBindings = async ( actionId: KeybindingActionId, @@ -262,133 +323,57 @@ export function ShortcutsPane(): React.JSX.Element { } const showPolicy = matchesSettingsSearch(searchQuery, TERMINAL_SHORTCUT_POLICY_SEARCH_ENTRY) - const showCtrlTab = matchesSettingsSearch(searchQuery, CTRL_TAB_BEHAVIOR_SEARCH_ENTRY) return ( -
-
+
+
- {showPolicy || showCtrlTab ? ( -
+
+ + +
{showPolicy ? ( - - - void updateSettings({ - terminalShortcutPolicy: value as TerminalShortcutPolicy - }) - } - > - - - - - Orca first - Terminal first - - - } - /> - + updateSettings={updateSettings} + /> ) : null} - {showCtrlTab ? ( - - - void updateSettings({ ctrlTabOrderMode: value as CtrlTabOrderMode }) - } - > - - - - - Most recent - Tab strip order - - - } - /> - - ) : null} + + + { + setRecordingActionId(actionId) + clearError(actionId) + }} + onCancelRecording={() => setRecordingActionId(null)} + onCapture={(actionId, input) => void captureBinding(actionId, input)} + onClearError={clearError} + onDisable={(actionId) => void disableBinding(actionId)} + onReset={(actionId) => void resetBinding(actionId)} + />
- ) : null} - - - -
- {groups - .filter((group) => matchesSettingsSearch(searchQuery, groupEntries[group.title] ?? [])) - .map((group) => ( -
-

- {group.title} -

-
- {group.items.map((item) => { - const effective = getEffectiveKeybindingsForAction( - item.id, - platform, - keybindings - ) - const modified = hasOwnBindingOverride(keybindings, item.id) - const warnings = conflictByAction.get(item.id) ?? [] - const terminalStatus = getShortcutTerminalStatus( - item, - terminalShortcutPolicy, - effective.length > 0 - ) - - return ( - { - setRecordingActionId(actionId) - clearError(actionId) - }} - onCancelRecording={() => setRecordingActionId(null)} - onCapture={(actionId, input) => void captureBinding(actionId, input)} - onClearError={clearError} - onDisable={(actionId) => void disableBinding(actionId)} - onReset={(actionId) => void resetBinding(actionId)} - /> - ) - })} -
-
- ))}
diff --git a/src/renderer/src/components/settings/general-search.ts b/src/renderer/src/components/settings/general-search.ts index f1fdfc259da..22b935a72fb 100644 --- a/src/renderer/src/components/settings/general-search.ts +++ b/src/renderer/src/components/settings/general-search.ts @@ -61,6 +61,23 @@ export const GENERAL_EDITOR_SEARCH_ENTRIES: SettingsSearchEntry[] = [ } ] +export const GENERAL_NAVIGATION_SEARCH_ENTRIES: SettingsSearchEntry[] = [ + { + title: 'Tab Order', + description: 'Recent or tab strip.', + keywords: [ + 'recent tab order', + 'tab', + 'ctrl', + 'control', + 'recent', + 'mru', + 'sequential', + 'switch' + ] + } +] + export const GENERAL_CLI_SEARCH_ENTRIES: SettingsSearchEntry[] = [ { title: 'Shell command', @@ -119,6 +136,7 @@ export const GENERAL_SUPPORT_SEARCH_ENTRIES: SettingsSearchEntry[] = [ export const GENERAL_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [ ...GENERAL_WORKSPACE_SEARCH_ENTRIES, + ...GENERAL_NAVIGATION_SEARCH_ENTRIES, ...GENERAL_EDITOR_SEARCH_ENTRIES, ...GENERAL_CLI_SEARCH_ENTRIES, ...GENERAL_CACHE_TIMER_SEARCH_ENTRIES, diff --git a/src/renderer/src/components/settings/shortcuts-search.ts b/src/renderer/src/components/settings/shortcuts-search.ts index 945f1224f65..c5108c41de5 100644 --- a/src/renderer/src/components/settings/shortcuts-search.ts +++ b/src/renderer/src/components/settings/shortcuts-search.ts @@ -1,12 +1,6 @@ import { KEYBINDING_DEFINITIONS } from '../../../../shared/keybindings' import type { SettingsSearchEntry } from './settings-search' -export const CTRL_TAB_BEHAVIOR_SEARCH_ENTRY: SettingsSearchEntry = { - title: 'Recent Tab Order', - description: 'Choose recent or sequential tab switching.', - keywords: ['shortcut', 'tab', 'ctrl', 'control', 'recent', 'mru', 'sequential', 'switch'] -} - export const TERMINAL_SHORTCUT_POLICY_SEARCH_ENTRY: SettingsSearchEntry = { title: 'Shortcuts in Terminal', description: 'Choose whether Orca or the focused terminal wins when shortcuts overlap.', @@ -29,6 +23,5 @@ export const SHORTCUTS_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [ description: `${item.group} shortcut`, keywords: [...item.searchKeywords] })), - TERMINAL_SHORTCUT_POLICY_SEARCH_ENTRY, - CTRL_TAB_BEHAVIOR_SEARCH_ENTRY + TERMINAL_SHORTCUT_POLICY_SEARCH_ENTRY ] diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index 43e5b2175d2..c54e643c68d 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -48,8 +48,6 @@ import { type LinkedWorkItemSummary, type SetupConfig } from '@/lib/new-workspace' -import { getShortcutPlatform } from '@/lib/shortcut-platform' -import { useShortcutLabel } from '@/hooks/useShortcutLabel' import { getFullComposerCreateDisabled, getQuickComposerCreateDisabled @@ -77,7 +75,6 @@ import { } from '@/lib/workspace-create-error-format' import type { SshConnectionStatus } from '../../../shared/ssh-types' import { resolveComposerBranchSelection } from './composer-branch-selection' -import { keybindingMatchesAction } from '../../../shared/keybindings' export type UseComposerStateOptions = { initialRepoId?: string @@ -134,7 +131,6 @@ export type ComposerCardProps = { onClearSmartNameSelection: () => void agentPrompt: string onAgentPromptChange: (value: string) => void - onPromptKeyDown: (event: React.KeyboardEvent) => void /** Rendered issueCommand template to preview inside the empty prompt * textarea when the user has linked a work item but not typed anything. */ linkedOnlyTemplatePreview: string | null @@ -142,7 +138,6 @@ export type ComposerCardProps = { getAttachmentLabel: (pathValue: string) => string onAddAttachment: () => void onRemoveAttachment: (pathValue: string) => void - addAttachmentShortcut: string linkedWorkItem: LinkedWorkItemSummary | null onRemoveLinkedWorkItem: () => void linkPopoverOpen: boolean @@ -1372,28 +1367,6 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS } }, []) - const handlePromptKeyDown = useCallback( - (event: React.KeyboardEvent): void => { - if ( - !keybindingMatchesAction( - 'composer.addAttachment', - event, - getShortcutPlatform(), - useAppStore.getState().keybindings - ) - ) { - return - } - - // Why: the attachment picker should only steal Cmd/Ctrl+U while the user - // is composing a prompt, so the shortcut is scoped to the textarea rather - // than registered globally for the whole new-workspace surface. - event.preventDefault() - void handleAddAttachment() - }, - [handleAddAttachment] - ) - const handleRepoChange = useCallback( (value: string): void => { if (value === repoId) { @@ -2153,8 +2126,6 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS createGateMode === 'quick' ? getQuickComposerCreateDisabled(createGateInput) : getFullComposerCreateDisabled(createGateInput) - const addAttachmentShortcut = useShortcutLabel('composer.addAttachment') - const cardProps: ComposerCardProps = { eligibleRepos, repoId, @@ -2170,14 +2141,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS onClearSmartNameSelection: handleClearSmartNameSelection, agentPrompt, onAgentPromptChange: setAgentPrompt, - onPromptKeyDown: handlePromptKeyDown, linkedOnlyTemplatePreview: shouldApplyLinkedOnlyTemplate ? linkedOnlyTemplatePrompt : null, attachmentPaths, getAttachmentLabel, onAddAttachment: () => void handleAddAttachment(), onRemoveAttachment: (pathValue) => setAttachmentPaths((current) => current.filter((currentPath) => currentPath !== pathValue)), - addAttachmentShortcut, linkedWorkItem, onRemoveLinkedWorkItem: handleRemoveLinkedWorkItem, linkPopoverOpen, diff --git a/src/shared/keybindings.ts b/src/shared/keybindings.ts index e170f928ca6..17e1021cd3a 100644 --- a/src/shared/keybindings.ts +++ b/src/shared/keybindings.ts @@ -71,7 +71,6 @@ export type KeybindingActionId = | 'fileExplorer.copyPath' | 'fileExplorer.copyRelativePath' | 'fileExplorer.delete' - | 'composer.addAttachment' | 'settings.search' | 'terminal.copySelection' | 'terminal.paste' @@ -566,14 +565,6 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [ }, allowBareKeybindings: true }, - { - id: 'composer.addAttachment', - title: 'Add Attachment', - group: 'Composer', - scope: 'composer', - searchKeywords: ['shortcut', 'composer', 'attachment', 'upload'], - defaultBindings: platformBindings(['Mod+U']) - }, { id: 'settings.search', title: 'Search Settings',