refresh.ts
utils/plugins/refresh.ts
No strong subsystem tag
216
Lines
8537
Bytes
2
Exports
17
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 216 lines, 17 detected imports, and 2 detected exports.
Important relationships
Detected exports
RefreshActivePluginsResultrefreshActivePlugins
Keywords
serverspluginerrorslengthenabledpluginsappstateloadallpluginsprevagentdefinitions
Detected imports
../../bootstrap/state.js../../commands.js../../services/lsp/manager.js../../state/AppState.js../../tools/AgentTool/loadAgentsDir.js../../tools/AgentTool/loadAgentsDir.js../../types/plugin.js../debug.js../errors.js../log.js./cacheUtils.js./loadPluginCommands.js./loadPluginHooks.js./lspPluginIntegration.js./mcpPluginIntegration.js./orphanedPluginFilter.js./pluginLoader.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* Layer-3 refresh primitive: swap active plugin components in the running session.
*
* Three-layer model (see reconciler.ts for Layer-2):
* - Layer 1: intent (settings)
* - Layer 2: materialization (~/.claude/plugins/) — reconcileMarketplaces()
* - Layer 3: active components (AppState) — this file
*
* Called from:
* - /reload-plugins command (interactive, user-initiated)
* - print.ts refreshPluginState() (headless, auto before first query with SYNC_PLUGIN_INSTALL)
* - performBackgroundPluginInstallations() (background, auto after new marketplace install)
*
* NOT called from:
* - useManagePlugins needsRefresh effect — interactive mode shows a notification;
* user explicitly runs /reload-plugins (PR 5c)
* - /plugin menu — sets needsRefresh, user runs /reload-plugins (PR 5b)
*/
import { getOriginalCwd } from '../../bootstrap/state.js'
import type { Command } from '../../commands.js'
import { reinitializeLspServerManager } from '../../services/lsp/manager.js'
import type { AppState } from '../../state/AppState.js'
import type { AgentDefinitionsResult } from '../../tools/AgentTool/loadAgentsDir.js'
import { getAgentDefinitionsWithOverrides } from '../../tools/AgentTool/loadAgentsDir.js'
import type { PluginError } from '../../types/plugin.js'
import { logForDebugging } from '../debug.js'
import { errorMessage } from '../errors.js'
import { logError } from '../log.js'
import { clearAllCaches } from './cacheUtils.js'
import { getPluginCommands } from './loadPluginCommands.js'
import { loadPluginHooks } from './loadPluginHooks.js'
import { loadPluginLspServers } from './lspPluginIntegration.js'
import { loadPluginMcpServers } from './mcpPluginIntegration.js'
import { clearPluginCacheExclusions } from './orphanedPluginFilter.js'
import { loadAllPlugins } from './pluginLoader.js'
type SetAppState = (updater: (prev: AppState) => AppState) => void
export type RefreshActivePluginsResult = {
enabled_count: number
disabled_count: number
command_count: number
agent_count: number
hook_count: number
mcp_count: number
/** LSP servers provided by enabled plugins. reinitializeLspServerManager()
* is called unconditionally so the manager picks these up (no-op if
* manager was never initialized). */
lsp_count: number
error_count: number
/** The refreshed agent definitions, for callers (e.g. print.ts) that also
* maintain a local mutable reference outside AppState. */
agentDefinitions: AgentDefinitionsResult
/** The refreshed plugin commands, same rationale as agentDefinitions. */
pluginCommands: Command[]
}
/**
* Refresh all active plugin components: commands, agents, hooks, MCP-reconnect
* trigger, AppState plugin arrays. Clears ALL plugin caches (unlike the old
* needsRefresh path which only cleared loadAllPlugins and returned stale data
* from downstream memoized loaders).
*
* Consumes plugins.needsRefresh (sets to false).
* Increments mcp.pluginReconnectKey so useManageMCPConnections effects re-run
* and pick up new plugin MCP servers.
*
* LSP: if plugins now contribute LSP servers, reinitializeLspServerManager()
* re-reads config. Servers are lazy-started so this is just config parsing.
*/
export async function refreshActivePlugins(
setAppState: SetAppState,
): Promise<RefreshActivePluginsResult> {
logForDebugging('refreshActivePlugins: clearing all plugin caches')
clearAllCaches()
// Orphan exclusions are session-frozen by default, but /reload-plugins is
// an explicit "disk changed, re-read it" signal — recompute them too.
clearPluginCacheExclusions()
// Sequence the full load before cache-only consumers. Before #23693 all
// three shared loadAllPlugins()'s memoize promise so Promise.all was a
// no-op race. After #23693 getPluginCommands/getAgentDefinitions call
// loadAllPluginsCacheOnly (separate memoize) — racing them means they
// read installed_plugins.json before loadAllPlugins() has cloned+cached
// the plugin, returning plugin-cache-miss. loadAllPlugins warms the
// cache-only memoize on completion, so the awaits below are ~free.
const pluginResult = await loadAllPlugins()
const [pluginCommands, agentDefinitions] = await Promise.all([
getPluginCommands(),
getAgentDefinitionsWithOverrides(getOriginalCwd()),
])
const { enabled, disabled, errors } = pluginResult
// Populate mcpServers/lspServers on each enabled plugin. These are lazy
// cache slots NOT filled by loadAllPlugins() — they're written later by
// extractMcpServersFromPlugins/getPluginLspServers, which races with this.
// Loading here gives accurate metrics AND warms the cache slots so the MCP
// connection manager (triggered by pluginReconnectKey bump) sees the servers
// without re-parsing manifests. Errors are pushed to the shared errors array.
const [mcpCounts, lspCounts] = await Promise.all([
Promise.all(
enabled.map(async p => {
if (p.mcpServers) return Object.keys(p.mcpServers).length
const servers = await loadPluginMcpServers(p, errors)
if (servers) p.mcpServers = servers
return servers ? Object.keys(servers).length : 0
}),
),
Promise.all(
enabled.map(async p => {
if (p.lspServers) return Object.keys(p.lspServers).length
const servers = await loadPluginLspServers(p, errors)
if (servers) p.lspServers = servers
return servers ? Object.keys(servers).length : 0
}),
),
])
const mcp_count = mcpCounts.reduce((sum, n) => sum + n, 0)
const lsp_count = lspCounts.reduce((sum, n) => sum + n, 0)
setAppState(prev => ({
...prev,
plugins: {
...prev.plugins,
enabled,
disabled,
commands: pluginCommands,
errors: mergePluginErrors(prev.plugins.errors, errors),
needsRefresh: false,
},
agentDefinitions,
mcp: {
...prev.mcp,
pluginReconnectKey: prev.mcp.pluginReconnectKey + 1,
},
}))
// Re-initialize LSP manager so newly-loaded plugin LSP servers are picked
// up. No-op if LSP was never initialized (headless subcommand path).
// Unconditional so removing the last LSP plugin also clears stale config.
// Fixes issue #15521: LSP manager previously read a stale memoized
// loadAllPlugins() result from before marketplaces were reconciled.
reinitializeLspServerManager()
// clearAllCaches() prunes removed-plugin hooks; this does the FULL swap
// (adds hooks from newly-enabled plugins too). Catching here so
// hook_load_failed can feed error_count; a failure doesn't lose the
// plugin/command/agent data above (hooks go to STATE.registeredHooks, not
// AppState).
let hook_load_failed = false
try {
await loadPluginHooks()
} catch (e) {
hook_load_failed = true
logError(e)
logForDebugging(
`refreshActivePlugins: loadPluginHooks failed: ${errorMessage(e)}`,
)
}
const hook_count = enabled.reduce((sum, p) => {
if (!p.hooksConfig) return sum
return (
sum +
Object.values(p.hooksConfig).reduce(
(s, matchers) =>
s + (matchers?.reduce((h, m) => h + m.hooks.length, 0) ?? 0),
0,
)
)
}, 0)
logForDebugging(
`refreshActivePlugins: ${enabled.length} enabled, ${pluginCommands.length} commands, ${agentDefinitions.allAgents.length} agents, ${hook_count} hooks, ${mcp_count} MCP, ${lsp_count} LSP`,
)
return {
enabled_count: enabled.length,
disabled_count: disabled.length,
command_count: pluginCommands.length,
agent_count: agentDefinitions.allAgents.length,
hook_count,
mcp_count,
lsp_count,
error_count: errors.length + (hook_load_failed ? 1 : 0),
agentDefinitions,
pluginCommands,
}
}
/**
* Merge fresh plugin-load errors with existing errors, preserving LSP and
* plugin-component errors that were recorded by other systems and
* deduplicating. Same logic as refreshPlugins()/updatePluginState(), extracted
* so refresh.ts doesn't leave those errors stranded.
*/
function mergePluginErrors(
existing: PluginError[],
fresh: PluginError[],
): PluginError[] {
const preserved = existing.filter(
e => e.source === 'lsp-manager' || e.source.startsWith('plugin:'),
)
const freshKeys = new Set(fresh.map(errorKey))
const deduped = preserved.filter(e => !freshKeys.has(errorKey(e)))
return [...deduped, ...fresh]
}
function errorKey(e: PluginError): string {
return e.type === 'generic-error'
? `generic-error:${e.source}:${e.error}`
: `${e.type}:${e.source}`
}