source dump of claude code
at main 215 lines 8.5 kB view raw
1/** 2 * Layer-3 refresh primitive: swap active plugin components in the running session. 3 * 4 * Three-layer model (see reconciler.ts for Layer-2): 5 * - Layer 1: intent (settings) 6 * - Layer 2: materialization (~/.claude/plugins/) — reconcileMarketplaces() 7 * - Layer 3: active components (AppState) — this file 8 * 9 * Called from: 10 * - /reload-plugins command (interactive, user-initiated) 11 * - print.ts refreshPluginState() (headless, auto before first query with SYNC_PLUGIN_INSTALL) 12 * - performBackgroundPluginInstallations() (background, auto after new marketplace install) 13 * 14 * NOT called from: 15 * - useManagePlugins needsRefresh effect — interactive mode shows a notification; 16 * user explicitly runs /reload-plugins (PR 5c) 17 * - /plugin menu — sets needsRefresh, user runs /reload-plugins (PR 5b) 18 */ 19 20import { getOriginalCwd } from '../../bootstrap/state.js' 21import type { Command } from '../../commands.js' 22import { reinitializeLspServerManager } from '../../services/lsp/manager.js' 23import type { AppState } from '../../state/AppState.js' 24import type { AgentDefinitionsResult } from '../../tools/AgentTool/loadAgentsDir.js' 25import { getAgentDefinitionsWithOverrides } from '../../tools/AgentTool/loadAgentsDir.js' 26import type { PluginError } from '../../types/plugin.js' 27import { logForDebugging } from '../debug.js' 28import { errorMessage } from '../errors.js' 29import { logError } from '../log.js' 30import { clearAllCaches } from './cacheUtils.js' 31import { getPluginCommands } from './loadPluginCommands.js' 32import { loadPluginHooks } from './loadPluginHooks.js' 33import { loadPluginLspServers } from './lspPluginIntegration.js' 34import { loadPluginMcpServers } from './mcpPluginIntegration.js' 35import { clearPluginCacheExclusions } from './orphanedPluginFilter.js' 36import { loadAllPlugins } from './pluginLoader.js' 37 38type SetAppState = (updater: (prev: AppState) => AppState) => void 39 40export type RefreshActivePluginsResult = { 41 enabled_count: number 42 disabled_count: number 43 command_count: number 44 agent_count: number 45 hook_count: number 46 mcp_count: number 47 /** LSP servers provided by enabled plugins. reinitializeLspServerManager() 48 * is called unconditionally so the manager picks these up (no-op if 49 * manager was never initialized). */ 50 lsp_count: number 51 error_count: number 52 /** The refreshed agent definitions, for callers (e.g. print.ts) that also 53 * maintain a local mutable reference outside AppState. */ 54 agentDefinitions: AgentDefinitionsResult 55 /** The refreshed plugin commands, same rationale as agentDefinitions. */ 56 pluginCommands: Command[] 57} 58 59/** 60 * Refresh all active plugin components: commands, agents, hooks, MCP-reconnect 61 * trigger, AppState plugin arrays. Clears ALL plugin caches (unlike the old 62 * needsRefresh path which only cleared loadAllPlugins and returned stale data 63 * from downstream memoized loaders). 64 * 65 * Consumes plugins.needsRefresh (sets to false). 66 * Increments mcp.pluginReconnectKey so useManageMCPConnections effects re-run 67 * and pick up new plugin MCP servers. 68 * 69 * LSP: if plugins now contribute LSP servers, reinitializeLspServerManager() 70 * re-reads config. Servers are lazy-started so this is just config parsing. 71 */ 72export async function refreshActivePlugins( 73 setAppState: SetAppState, 74): Promise<RefreshActivePluginsResult> { 75 logForDebugging('refreshActivePlugins: clearing all plugin caches') 76 clearAllCaches() 77 // Orphan exclusions are session-frozen by default, but /reload-plugins is 78 // an explicit "disk changed, re-read it" signal — recompute them too. 79 clearPluginCacheExclusions() 80 81 // Sequence the full load before cache-only consumers. Before #23693 all 82 // three shared loadAllPlugins()'s memoize promise so Promise.all was a 83 // no-op race. After #23693 getPluginCommands/getAgentDefinitions call 84 // loadAllPluginsCacheOnly (separate memoize) — racing them means they 85 // read installed_plugins.json before loadAllPlugins() has cloned+cached 86 // the plugin, returning plugin-cache-miss. loadAllPlugins warms the 87 // cache-only memoize on completion, so the awaits below are ~free. 88 const pluginResult = await loadAllPlugins() 89 const [pluginCommands, agentDefinitions] = await Promise.all([ 90 getPluginCommands(), 91 getAgentDefinitionsWithOverrides(getOriginalCwd()), 92 ]) 93 94 const { enabled, disabled, errors } = pluginResult 95 96 // Populate mcpServers/lspServers on each enabled plugin. These are lazy 97 // cache slots NOT filled by loadAllPlugins() — they're written later by 98 // extractMcpServersFromPlugins/getPluginLspServers, which races with this. 99 // Loading here gives accurate metrics AND warms the cache slots so the MCP 100 // connection manager (triggered by pluginReconnectKey bump) sees the servers 101 // without re-parsing manifests. Errors are pushed to the shared errors array. 102 const [mcpCounts, lspCounts] = await Promise.all([ 103 Promise.all( 104 enabled.map(async p => { 105 if (p.mcpServers) return Object.keys(p.mcpServers).length 106 const servers = await loadPluginMcpServers(p, errors) 107 if (servers) p.mcpServers = servers 108 return servers ? Object.keys(servers).length : 0 109 }), 110 ), 111 Promise.all( 112 enabled.map(async p => { 113 if (p.lspServers) return Object.keys(p.lspServers).length 114 const servers = await loadPluginLspServers(p, errors) 115 if (servers) p.lspServers = servers 116 return servers ? Object.keys(servers).length : 0 117 }), 118 ), 119 ]) 120 const mcp_count = mcpCounts.reduce((sum, n) => sum + n, 0) 121 const lsp_count = lspCounts.reduce((sum, n) => sum + n, 0) 122 123 setAppState(prev => ({ 124 ...prev, 125 plugins: { 126 ...prev.plugins, 127 enabled, 128 disabled, 129 commands: pluginCommands, 130 errors: mergePluginErrors(prev.plugins.errors, errors), 131 needsRefresh: false, 132 }, 133 agentDefinitions, 134 mcp: { 135 ...prev.mcp, 136 pluginReconnectKey: prev.mcp.pluginReconnectKey + 1, 137 }, 138 })) 139 140 // Re-initialize LSP manager so newly-loaded plugin LSP servers are picked 141 // up. No-op if LSP was never initialized (headless subcommand path). 142 // Unconditional so removing the last LSP plugin also clears stale config. 143 // Fixes issue #15521: LSP manager previously read a stale memoized 144 // loadAllPlugins() result from before marketplaces were reconciled. 145 reinitializeLspServerManager() 146 147 // clearAllCaches() prunes removed-plugin hooks; this does the FULL swap 148 // (adds hooks from newly-enabled plugins too). Catching here so 149 // hook_load_failed can feed error_count; a failure doesn't lose the 150 // plugin/command/agent data above (hooks go to STATE.registeredHooks, not 151 // AppState). 152 let hook_load_failed = false 153 try { 154 await loadPluginHooks() 155 } catch (e) { 156 hook_load_failed = true 157 logError(e) 158 logForDebugging( 159 `refreshActivePlugins: loadPluginHooks failed: ${errorMessage(e)}`, 160 ) 161 } 162 163 const hook_count = enabled.reduce((sum, p) => { 164 if (!p.hooksConfig) return sum 165 return ( 166 sum + 167 Object.values(p.hooksConfig).reduce( 168 (s, matchers) => 169 s + (matchers?.reduce((h, m) => h + m.hooks.length, 0) ?? 0), 170 0, 171 ) 172 ) 173 }, 0) 174 175 logForDebugging( 176 `refreshActivePlugins: ${enabled.length} enabled, ${pluginCommands.length} commands, ${agentDefinitions.allAgents.length} agents, ${hook_count} hooks, ${mcp_count} MCP, ${lsp_count} LSP`, 177 ) 178 179 return { 180 enabled_count: enabled.length, 181 disabled_count: disabled.length, 182 command_count: pluginCommands.length, 183 agent_count: agentDefinitions.allAgents.length, 184 hook_count, 185 mcp_count, 186 lsp_count, 187 error_count: errors.length + (hook_load_failed ? 1 : 0), 188 agentDefinitions, 189 pluginCommands, 190 } 191} 192 193/** 194 * Merge fresh plugin-load errors with existing errors, preserving LSP and 195 * plugin-component errors that were recorded by other systems and 196 * deduplicating. Same logic as refreshPlugins()/updatePluginState(), extracted 197 * so refresh.ts doesn't leave those errors stranded. 198 */ 199function mergePluginErrors( 200 existing: PluginError[], 201 fresh: PluginError[], 202): PluginError[] { 203 const preserved = existing.filter( 204 e => e.source === 'lsp-manager' || e.source.startsWith('plugin:'), 205 ) 206 const freshKeys = new Set(fresh.map(errorKey)) 207 const deduped = preserved.filter(e => !freshKeys.has(errorKey(e))) 208 return [...deduped, ...fresh] 209} 210 211function errorKey(e: PluginError): string { 212 return e.type === 'generic-error' 213 ? `generic-error:${e.source}:${e.error}` 214 : `${e.type}:${e.source}` 215}