| import { | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| logEvent, | |
| } from '../services/analytics/index.js' | |
| import { loadKeybindingsSync } from './loadUserBindings.js' | |
| import { getBindingDisplayText } from './resolver.js' | |
| import type { KeybindingContextName } from './types.js' | |
| // TODO(keybindings-migration): Remove fallback parameter after migration is | |
| // complete and we've confirmed no 'keybinding_fallback_used' events are being | |
| // logged. The fallback exists as a safety net during migration - if bindings | |
| // fail to load or an action isn't found, we fall back to hardcoded values. | |
| // Once stable, callers should be able to trust that getBindingDisplayText | |
| // always returns a value for known actions, and we can remove this defensive | |
| // pattern. | |
| // Track which action+context pairs have already logged a fallback event | |
| // to avoid duplicate events from repeated calls in non-React contexts. | |
| const LOGGED_FALLBACKS = new Set<string>() | |
| /** | |
| * Get the display text for a configured shortcut without React hooks. | |
| * Use this in non-React contexts (commands, services, etc.). | |
| * | |
| * This lives in its own module (not useShortcutDisplay.ts) so that | |
| * non-React callers like query/stopHooks.ts don't pull React into their | |
| * module graph via the sibling hook. | |
| * | |
| * @param action - The action name (e.g., 'app:toggleTranscript') | |
| * @param context - The keybinding context (e.g., 'Global') | |
| * @param fallback - Fallback text if binding not found | |
| * @returns The configured shortcut display text | |
| * | |
| * @example | |
| * const expandShortcut = getShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o') | |
| * // Returns the user's configured binding, or 'ctrl+o' as default | |
| */ | |
| export function getShortcutDisplay( | |
| action: string, | |
| context: KeybindingContextName, | |
| fallback: string, | |
| ): string { | |
| const bindings = loadKeybindingsSync() | |
| const resolved = getBindingDisplayText(action, context, bindings) | |
| if (resolved === undefined) { | |
| const key = `${action}:${context}` | |
| if (!LOGGED_FALLBACKS.has(key)) { | |
| LOGGED_FALLBACKS.add(key) | |
| logEvent('tengu_keybinding_fallback_used', { | |
| action: | |
| action as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| context: | |
| context as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| fallback: | |
| fallback as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| reason: | |
| 'action_not_found' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| } | |
| return fallback | |
| } | |
| return resolved | |
| } | |