/** * Shared state machine + install helper for plugin-recommendation hooks * (LSP, claude-code-hint). Centralizes the gate chain, async-guard, * and success/failure notification JSX so new sources stay small. */ import figures from 'figures'; import * as React from 'react'; import { getIsRemoteMode } from '../bootstrap/state.js'; import type { useNotifications } from '../context/notifications.js'; import { Text } from '@anthropic/ink'; import { logError } from '../utils/log.js'; import { getPluginById } from '../utils/plugins/marketplaceManager.js'; type AddNotification = ReturnType['addNotification']; type PluginData = NonNullable>>; /** * Call tryResolve inside a useEffect; it applies standard gates (remote * mode, already-showing, in-flight) then runs resolve(). Non-null return * becomes the recommendation. Include tryResolve in effect deps — its * identity tracks recommendation, so clearing re-triggers resolution. */ export function usePluginRecommendationBase(): { recommendation: T | null; clearRecommendation: () => void; tryResolve: (resolve: () => Promise) => void; } { const [recommendation, setRecommendation] = React.useState(null); const isCheckingRef = React.useRef(false); const tryResolve = React.useCallback( (resolve: () => Promise) => { if (getIsRemoteMode()) return; if (recommendation) return; if (isCheckingRef.current) return; isCheckingRef.current = true; void resolve() .then(rec => { if (rec) setRecommendation(rec); }) .catch(logError) .finally(() => { isCheckingRef.current = false; }); }, [recommendation], ); const clearRecommendation = React.useCallback(() => setRecommendation(null), []); return { recommendation, clearRecommendation, tryResolve }; } /** Look up plugin, run install(), emit standard success/failure notification. */ export async function installPluginAndNotify( pluginId: string, pluginName: string, keyPrefix: string, addNotification: AddNotification, install: (pluginData: PluginData) => Promise, ): Promise { try { const pluginData = await getPluginById(pluginId); if (!pluginData) { throw new Error(`Plugin ${pluginId} not found in marketplace`); } await install(pluginData); addNotification({ key: `${keyPrefix}-installed`, jsx: ( {figures.tick} {pluginName} installed · restart to apply ), priority: 'immediate', timeoutMs: 5000, }); } catch (error) { logError(error); addNotification({ key: `${keyPrefix}-install-failed`, jsx: Failed to install {pluginName}, priority: 'immediate', timeoutMs: 5000, }); } }