Keep unloaded plugins in list with Reload button

This commit is contained in:
2026-04-18 20:41:43 +10:00
parent 11b428e25b
commit e862ec5f70

View File

@@ -75,31 +75,55 @@
}, },
}; };
// ---------------------------------------------------------------------------
// Plugin context
// ---------------------------------------------------------------------------
const context = createPluginContext(
{
pluginId: 'example-plugin',
eventClient: eventEmitter,
onNotify: (opts) =>
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
},
registry
);
// Intercept context logging so it appears in the UI panel
context.log = (...args) => {
console.log('[Plugin example-plugin]', ...args);
registry.addLog('example-plugin', 'log', args);
addLog(args.map(String).join(' '), 'log');
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Register + load // Register + load
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
registry.registerPlugin('example-plugin', plugin, context);
await plugin.onLoad(context); /**
* Persistent record of every plugin we have ever seen, keyed by plugin id.
* Survives unload so the Installed list can show a Reload button.
*
* @type {Map<string, { name: string, version: string, pluginModule: object, filePath?: string, downloadUrl?: string }>}
*/
const knownPlugins = new Map();
/**
* Creates a plugin context wired to the shared registry and event emitter,
* registers and loads the plugin, then records it in knownPlugins.
* @param {string} id
* @param {object} pluginModule
* @param {{ filePath?: string, downloadUrl?: string }} [source]
*/
async function registerAndLoad(id, pluginModule, source = {}) {
const ctx = createPluginContext(
{
pluginId: id,
eventClient: eventEmitter,
onNotify: (opts) =>
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
},
registry
);
ctx.log = (...args) => {
console.log(`[Plugin ${id}]`, ...args);
registry.addLog(id, 'log', args);
addLog(args.map(String).join(' '), 'log');
};
registry.registerPlugin(id, pluginModule, ctx);
await pluginModule.onLoad(ctx);
knownPlugins.set(id, {
name: pluginModule.name ?? id,
version: pluginModule.version ?? '?',
pluginModule,
...source,
});
}
await registerAndLoad('example-plugin', plugin, {
filePath: pluginPath,
});
document.getElementById('status-badge').textContent = 'example-plugin loaded'; document.getElementById('status-badge').textContent = 'example-plugin loaded';
document.getElementById('status-badge').classList.add('loaded'); document.getElementById('status-badge').classList.add('loaded');
@@ -109,39 +133,67 @@
// Plugin Manager panel // Plugin Manager panel
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** Renders the plugin directory card list. */ /** Renders the installed-plugins card list, including unloaded entries. */
function refreshPluginList() { function refreshPluginList() {
const listEl = document.getElementById('plugin-list'); const listEl = document.getElementById('plugin-list');
const plugins = registry.getRegisteredPlugins(); const loadedMap = new Map(registry.getRegisteredPlugins().map(p => [p.id, p]));
if (!plugins.length) { if (!knownPlugins.size) {
listEl.innerHTML = '<div id="no-plugins-msg" style="color:#555;font-size:12px;text-align:center;padding:20px 0;">No plugins loaded</div>'; listEl.innerHTML = '<div id="no-plugins-msg" style="color:#555;font-size:12px;text-align:center;padding:20px 0;">No plugins loaded</div>';
return; return;
} }
listEl.innerHTML = plugins.map(p => ` listEl.innerHTML = Array.from(knownPlugins.entries()).map(([id, known]) => {
<div class="plugin-card" data-id="${p.id}"> const live = loadedMap.get(id);
<div class="plugin-card-header"> const loaded = !!live;
<span class="plugin-name">${p.name}</span> const statusBadge = loaded
<span class="plugin-version">v${p.version}</span> ? '<span class="badge green">loaded</span>'
</div> : '<span class="badge red">unloaded</span>';
<div style="font-size:10px;color:#7986cb;font-family:monospace;">${p.id}</div> const featureBadges = live ? `
<div class="plugin-badges"> ${live.commandCount ? `<span class="badge">${live.commandCount} cmd${live.commandCount !== 1 ? 's' : ''}</span>` : ''}
<span class="badge green">loaded</span> ${live.interceptorCount ? `<span class="badge">${live.interceptorCount} interceptor${live.interceptorCount !== 1 ? 's' : ''}</span>` : ''}
${p.commandCount ? `<span class="badge">${p.commandCount} cmd${p.commandCount !== 1 ? 's' : ''}</span>` : ''} ${live.themeCount ? `<span class="badge amber">${live.themeCount} theme${live.themeCount !== 1 ? 's' : ''}</span>` : ''}
${p.interceptorCount ? `<span class="badge">${p.interceptorCount} interceptor${p.interceptorCount !== 1 ? 's' : ''}</span>` : ''} ${live.hasSettings ? '<span class="badge">has settings</span>' : ''}
${p.themeCount ? `<span class="badge amber">${p.themeCount} theme${p.themeCount !== 1 ? 's' : ''}</span>` : ''} ` : '';
${p.hasSettings ? `<span class="badge">has settings</span>` : ''} const actionBtn = loaded
</div> ? `<button class="plugin-unload-btn" data-action="unload" data-id="${id}">Unload</button>`
<button class="plugin-unload-btn" data-id="${p.id}">Unload</button> : `<button class="plugin-unload-btn" style="background:#1a4a2e;color:#4caf50;" data-action="reload" data-id="${id}">Reload</button>`;
</div>
`).join('');
listEl.querySelectorAll('.plugin-unload-btn').forEach(btn => { return `
<div class="plugin-card" data-id="${id}">
<div class="plugin-card-header">
<span class="plugin-name">${known.name}</span>
<span class="plugin-version">v${known.version}</span>
</div>
<div style="font-size:10px;color:#7986cb;font-family:monospace;">${id}</div>
<div class="plugin-badges">${statusBadge}${featureBadges}</div>
${actionBtn}
</div>
`;
}).join('');
listEl.querySelectorAll('[data-action]').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
const id = btn.dataset.id; const id = btn.dataset.id;
await registry.unregisterPlugin(id); const action = btn.dataset.action;
addLog(`Plugin unloaded: ${id}`, 'warn');
if (action === 'unload') {
await registry.unregisterPlugin(id);
addLog(`Plugin unloaded: ${id}`, 'warn');
} else if (action === 'reload') {
const known = knownPlugins.get(id);
if (!known) return;
try {
await registerAndLoad(id, known.pluginModule, {
filePath: known.filePath,
downloadUrl: known.downloadUrl,
});
addLog(`Plugin reloaded: ${id}`, 'success');
} catch (err) {
addLog(`Failed to reload ${id}: ${err.message}`, 'error');
}
}
refreshPluginList(); refreshPluginList();
refreshCommandList(); refreshCommandList();
renderSettings(); renderSettings();
@@ -167,27 +219,11 @@
} }
const id = p.id ?? path.basename(path.dirname(filePath)); const id = p.id ?? path.basename(path.dirname(filePath));
if (registry.getRegisteredPlugins().some(r => r.id === id)) { if (knownPlugins.has(id) && registry.getRegisteredPlugins().some(r => r.id === id)) {
throw new Error(`Plugin with id "${id}" is already loaded`); throw new Error(`Plugin with id "${id}" is already loaded`);
} }
const ctx = createPluginContext( await registerAndLoad(id, p, { filePath });
{
pluginId: id,
eventClient: eventEmitter,
onNotify: (opts) =>
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
},
registry
);
ctx.log = (...args) => {
console.log(`[Plugin ${id}]`, ...args);
registry.addLog(id, 'log', args);
addLog(args.map(String).join(' '), 'log');
};
registry.registerPlugin(id, p, ctx);
await p.onLoad(ctx);
addLog(`Plugin loaded from path: ${id}`, 'success'); addLog(`Plugin loaded from path: ${id}`, 'success');
refreshPluginList(); refreshPluginList();
@@ -321,26 +357,9 @@
throw new Error(`Plugin ${pluginId} does not export a valid plugin object`); throw new Error(`Plugin ${pluginId} does not export a valid plugin object`);
} }
const id = pluginId; await registerAndLoad(pluginId, p, { downloadUrl });
const ctx = createPluginContext(
{
pluginId: id,
eventClient: eventEmitter,
onNotify: (opts) =>
addLog(`[${opts.type?.toUpperCase() ?? 'NOTIFY'}] ${opts.title}: ${opts.body}`, 'info'),
},
registry
);
ctx.log = (...args) => {
console.log(`[Plugin ${id}]`, ...args);
registry.addLog(id, 'log', args);
addLog(args.map(String).join(' '), 'log');
};
registry.registerPlugin(id, p, ctx); addLog(`Plugin installed from directory: ${pluginId}`, 'success');
await p.onLoad(ctx);
addLog(`Plugin installed from directory: ${id}`, 'success');
refreshPluginList(); refreshPluginList();
refreshCommandList(); refreshCommandList();
renderSettings(); renderSettings();