/ keybindings / shortcutFormat.ts
shortcutFormat.ts
 1  import {
 2    type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 3    logEvent,
 4  } from '../services/analytics/index.js'
 5  import { loadKeybindingsSync } from './loadUserBindings.js'
 6  import { getBindingDisplayText } from './resolver.js'
 7  import type { KeybindingContextName } from './types.js'
 8  
 9  // TODO(keybindings-migration): Remove fallback parameter after migration is
10  // complete and we've confirmed no 'keybinding_fallback_used' events are being
11  // logged. The fallback exists as a safety net during migration - if bindings
12  // fail to load or an action isn't found, we fall back to hardcoded values.
13  // Once stable, callers should be able to trust that getBindingDisplayText
14  // always returns a value for known actions, and we can remove this defensive
15  // pattern.
16  
17  // Track which action+context pairs have already logged a fallback event
18  // to avoid duplicate events from repeated calls in non-React contexts.
19  const LOGGED_FALLBACKS = new Set<string>()
20  
21  /**
22   * Get the display text for a configured shortcut without React hooks.
23   * Use this in non-React contexts (commands, services, etc.).
24   *
25   * This lives in its own module (not useShortcutDisplay.ts) so that
26   * non-React callers like query/stopHooks.ts don't pull React into their
27   * module graph via the sibling hook.
28   *
29   * @param action - The action name (e.g., 'app:toggleTranscript')
30   * @param context - The keybinding context (e.g., 'Global')
31   * @param fallback - Fallback text if binding not found
32   * @returns The configured shortcut display text
33   *
34   * @example
35   * const expandShortcut = getShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o')
36   * // Returns the user's configured binding, or 'ctrl+o' as default
37   */
38  export function getShortcutDisplay(
39    action: string,
40    context: KeybindingContextName,
41    fallback: string,
42  ): string {
43    const bindings = loadKeybindingsSync()
44    const resolved = getBindingDisplayText(action, context, bindings)
45    if (resolved === undefined) {
46      const key = `${action}:${context}`
47      if (!LOGGED_FALLBACKS.has(key)) {
48        LOGGED_FALLBACKS.add(key)
49        logEvent('tengu_keybinding_fallback_used', {
50          action:
51            action as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
52          context:
53            context as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
54          fallback:
55            fallback as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
56          reason:
57            'action_not_found' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
58        })
59      }
60      return fallback
61    }
62    return resolved
63  }