/**
* 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 }
}