/ keybindings / useShortcutDisplay.ts
useShortcutDisplay.ts
 1  import { useEffect, useRef } from 'react'
 2  import {
 3    type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 4    logEvent,
 5  } from '../services/analytics/index.js'
 6  import { useOptionalKeybindingContext } from './KeybindingContext.js'
 7  import type { KeybindingContextName } from './types.js'
 8  
 9  // TODO(keybindings-migration): Remove fallback parameter after migration is complete
10  // and we've confirmed no 'keybinding_fallback_used' events are being logged.
11  // The fallback exists as a safety net during migration - if bindings fail to load
12  // or an action isn't found, we fall back to hardcoded values. Once stable, callers
13  // should be able to trust that getBindingDisplayText always returns a value for
14  // known actions, and we can remove this defensive pattern.
15  
16  /**
17   * Hook to get the display text for a configured shortcut.
18   * Returns the configured binding or a fallback if unavailable.
19   *
20   * @param action - The action name (e.g., 'app:toggleTranscript')
21   * @param context - The keybinding context (e.g., 'Global')
22   * @param fallback - Fallback text if keybinding context unavailable
23   * @returns The configured shortcut display text
24   *
25   * @example
26   * const expandShortcut = useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o')
27   * // Returns the user's configured binding, or 'ctrl+o' as default
28   */
29  export function useShortcutDisplay(
30    action: string,
31    context: KeybindingContextName,
32    fallback: string,
33  ): string {
34    const keybindingContext = useOptionalKeybindingContext()
35    const resolved = keybindingContext?.getDisplayText(action, context)
36    const isFallback = resolved === undefined
37    const reason = keybindingContext ? 'action_not_found' : 'no_context'
38  
39    // Log fallback usage once per mount (not on every render) to avoid
40    // flooding analytics with events from frequent re-renders.
41    const hasLoggedRef = useRef(false)
42    useEffect(() => {
43      if (isFallback && !hasLoggedRef.current) {
44        hasLoggedRef.current = true
45        logEvent('tengu_keybinding_fallback_used', {
46          action:
47            action as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
48          context:
49            context as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
50          fallback:
51            fallback as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
52          reason:
53            reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
54        })
55      }
56    }, [isFallback, action, context, fallback, reason])
57  
58    return isFallback ? fallback : resolved
59  }