import * as React from 'react'; import { useInterval } from 'usehooks-ts'; import { getIsRemoteMode, getIsScrollDraining } from '../../bootstrap/state.js'; import { useNotifications } from '../../context/notifications.js'; import { Text } from '@anthropic/ink'; import { getInitializationStatus, getLspServerManager } from '../../services/lsp/manager.js'; import { useSetAppState } from '../../state/AppState.js'; import { logForDebugging } from '../../utils/debug.js'; import { isEnvTruthy } from '../../utils/envUtils.js'; const LSP_POLL_INTERVAL_MS = 5000; /** * Hook that polls LSP status and shows a notification when: * 1. Manager initialization fails * 2. Any LSP server enters an error state * * Also adds errors to appState.plugins.errors for /doctor display. * * Only active when ENABLE_LSP_TOOL is set. */ export function useLspInitializationNotification(): void { const { addNotification } = useNotifications(); const setAppState = useSetAppState(); // Lazy initializer — eager form re-evaluates isEnvTruthy on every REPL // render (the arg expression runs even though useState ignores it after // mount). Showed up as 7.2s isEnvTruthy self-time during PageUp spam // after #24498 swapped cheap !!process.env.X for isEnvTruthy(). const [shouldPoll, setShouldPoll] = React.useState(() => isEnvTruthy('true')); // Track which errors we've already notified about to avoid duplicates const notifiedErrorsRef = React.useRef>(new Set()); const addError = React.useCallback( (source: string, errorMessage: string) => { const errorKey = `${source}:${errorMessage}`; if (notifiedErrorsRef.current.has(errorKey)) { return; // Already notified } notifiedErrorsRef.current.add(errorKey); logForDebugging(`LSP error: ${source} - ${errorMessage}`); // Add error to appState.plugins.errors setAppState(prev => { // Check if this error already exists to avoid duplicates const existingKeys = new Set( prev.plugins.errors.map(e => { if (e.type === 'generic-error') { return `generic-error:${e.source}:${e.error}`; } return `${e.type}:${e.source}`; }), ); const stateErrorKey = `generic-error:${source}:${errorMessage}`; if (existingKeys.has(stateErrorKey)) { return prev; } return { ...prev, plugins: { ...prev.plugins, errors: [ ...prev.plugins.errors, { type: 'generic-error' as const, source, error: errorMessage, }, ], }, }; }); // Show notification - extract plugin name from source like "plugin:typescript-lsp:typescript" const displayName = source.startsWith('plugin:') ? (source.split(':')[1] ?? source) : source; addNotification({ key: `lsp-error-${source}`, jsx: ( <> LSP for {displayName} failed · /plugin for details ), priority: 'medium', timeoutMs: 8000, }); }, [addNotification, setAppState], ); const poll = React.useCallback(() => { if (getIsRemoteMode()) return; // Skip during scroll drain — iterating all LSP servers + setAppState // competes for the event loop with scroll frames. Next interval picks up. if (getIsScrollDraining()) return; const status = getInitializationStatus(); // Check manager initialization status if (status.status === 'failed') { addError('lsp-manager', status.error.message); setShouldPoll(false); return; } if (status.status === 'pending' || status.status === 'not-started') { // Still initializing, continue polling return; } // Manager initialized successfully - check for server errors const manager = getLspServerManager(); if (manager) { const servers = manager.getAllServers(); for (const [serverName, server] of servers) { if (server.state === 'error' && server.lastError) { addError(serverName, server.lastError.message); } } } // Continue polling to detect future server errors }, [addError]); useInterval(poll, shouldPoll ? LSP_POLL_INTERVAL_MS : null); // Initial poll on mount React.useEffect(() => { if (getIsRemoteMode() || !shouldPoll) return; poll(); }, [poll, shouldPoll]); }