/** * Surfaces plugin-install prompts driven by `` tags * that CLIs/SDKs emit to stderr. See docs/claude-code-hints.md. * * Show-once semantics: each plugin is prompted for at most once ever, * recorded in config regardless of yes/no. The pre-store gate in * maybeRecordPluginHint already dropped installed/shown/capped hints, so * anything that reaches this hook is worth resolving. */ import * as React from 'react' import { useNotifications } from '../context/notifications.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent, } from '../services/analytics/index.js' import { clearPendingHint, getPendingHintSnapshot, markShownThisSession, subscribeToPendingHint, } from '../utils/claudeCodeHints.js' import { logForDebugging } from '../utils/debug.js' import { disableHintRecommendations, markHintPluginShown, type PluginHintRecommendation, resolvePluginHint, } from '../utils/plugins/hintRecommendation.js' import { installPluginFromMarketplace } from '../utils/plugins/pluginInstallationHelpers.js' import { installPluginAndNotify, usePluginRecommendationBase, } from './usePluginRecommendationBase.js' type UseClaudeCodeHintRecommendationResult = { recommendation: PluginHintRecommendation | null handleResponse: (response: 'yes' | 'no' | 'disable') => void } export function useClaudeCodeHintRecommendation(): UseClaudeCodeHintRecommendationResult { const pendingHint = React.useSyncExternalStore( subscribeToPendingHint, getPendingHintSnapshot, ) const { addNotification } = useNotifications() const { recommendation, clearRecommendation, tryResolve } = usePluginRecommendationBase() React.useEffect(() => { if (!pendingHint) return tryResolve(async () => { const resolved = await resolvePluginHint(pendingHint) if (resolved) { logForDebugging( `[useClaudeCodeHintRecommendation] surfacing ${resolved.pluginId} from ${resolved.sourceCommand}`, ) markShownThisSession() } // Drop the slot — but only if it still holds the hint we just // resolved. A newer hint may have overwritten it during the async // lookup; don't clobber that. if (getPendingHintSnapshot() === pendingHint) { clearPendingHint() } return resolved }) }, [pendingHint, tryResolve]) const handleResponse = React.useCallback( (response: 'yes' | 'no' | 'disable') => { if (!recommendation) return // Record show-once here, not at resolution-time — the dialog may have // been blocked by a higher-priority focusedInputDialog and never // rendered. Auto-dismiss reaches this via onResponse('no'). markHintPluginShown(recommendation.pluginId) logEvent('tengu_plugin_hint_response', { _PROTO_plugin_name: recommendation.pluginName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, _PROTO_marketplace_name: recommendation.marketplaceName as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, response: response as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) switch (response) { case 'yes': { const { pluginId, pluginName, marketplaceName } = recommendation void installPluginAndNotify( pluginId, pluginName, 'hint-plugin', addNotification, async pluginData => { const result = await installPluginFromMarketplace({ pluginId, entry: pluginData.entry, marketplaceName, scope: 'user', trigger: 'hint', }) if (!result.success) { throw new Error(!result.success ? (result as { error: string }).error : 'Unknown error') } }, ) break } case 'disable': disableHintRecommendations() break case 'no': break } clearRecommendation() }, [recommendation, addNotification, clearRecommendation], ) return { recommendation, handleResponse } }