FeatureRegistry API reference¶
The FeatureRegistry manages all feature plugins, handles slash command routing, and coordinates event communication between plugins.
Class: FeatureRegistry¶
Central registry for feature plugins with priority-based slash command resolution.
Constructor¶
Creates a new FeatureRegistry instance.
Usage:
import { FeatureRegistry, PRIORITY } from '/static/js/feature-registry.js';
const registry = new FeatureRegistry();
Configuration¶
setAppContext()¶
Set the application context for dependency injection.
Must be called before registering any plugins.
Example:
Plugin management¶
register()¶
Register a feature plugin.
Parameters:
config object with:
id(string, required) - Unique plugin identifierfeature(Class, required) - FeaturePlugin class (not instance)slashCommands(Array, optional) - Slash command configurationspriority(number, optional) - Default priority (default:PRIORITY.BUILTIN)
Slash command config:
{
command: string, // e.g., '/mycommand'
handler: string, // Method name on feature instance
priority: number // Override default (optional)
}
Example:
import { MyFeature } from './my-feature.js';
import { PRIORITY } from '/static/js/feature-registry.js';
await registry.register({
id: 'my-feature',
feature: MyFeature,
slashCommands: [
{
command: '/mycommand',
handler: 'handleMyCommand',
},
{
command: '/other',
handler: 'handleOther',
priority: PRIORITY.OVERRIDE, // Higher priority than default
},
],
priority: PRIORITY.COMMUNITY,
});
What happens on registration:
- Instantiates feature class with AppContext
- Registers slash commands with conflict detection
- Subscribes to events via
getEventSubscriptions() - Calls
onLoad()lifecycle hook
Errors thrown:
- If
idis already registered:"Feature \"id\" is already registered" - If AppContext not set:
"AppContext must be set before registering features" - If slash command conflict with equal priority:
"Slash command conflict: /command..."
unregister()¶
Unregister a feature plugin and clean up.
Example:
What happens:
- Calls
onUnload()lifecycle hook - Removes all slash commands owned by plugin
- Removes event subscriptions
- Deletes feature instance
getFeature()¶
Get a registered feature instance by ID.
Example:
const committeeFeature = registry.getFeature('committee');
if (committeeFeature) {
committeeFeature.someMethod();
}
Slash command routing¶
handleSlashCommand()¶
Route a slash command to the appropriate feature.
Parameters:
command- Command string (e.g.,'/committee')args- Everything after the commandcontext- Execution context (e.g.,{ text: 'selected text' })
Returns:
trueif command was handledfalseif command not found
Example:
const handled = await registry.handleSlashCommand('/committee', 'What is the best approach?', {
text: 'Some context from selected nodes',
});
if (!handled) {
console.log('Unknown command');
}
Event flow:
- Emits
command:before(cancellable) - If not cancelled, calls handler method on feature
- On success, emits
command:after - On error, emits
command:errorand re-throws
getSlashCommands()¶
Get all registered slash commands.
Example:
const commands = registry.getSlashCommands();
console.log('Available:', commands);
// [''/committee', '/matrix', '/factcheck', ...]
Priority system¶
PRIORITY constants¶
export const PRIORITY = {
BUILTIN: 1000, // Built-in commands (default)
OFFICIAL: 500, // Official plugins
COMMUNITY: 100, // Third-party plugins
OVERRIDE: 2000, // Explicit override (highest)
};
Conflict resolution¶
When multiple plugins register the same slash command:
Equal priority → Error:
// Both have PRIORITY.BUILTIN (1000)
await registry.register({
id: 'plugin1',
feature: Plugin1,
slashCommands: [{ command: '/test', handler: 'handle' }],
priority: PRIORITY.BUILTIN,
});
await registry.register({
id: 'plugin2',
feature: Plugin2,
slashCommands: [{ command: '/test', handler: 'handle' }],
priority: PRIORITY.BUILTIN,
});
// ❌ Throws: "Slash command conflict: /test..."
Different priority → Higher wins:
// Plugin 1: PRIORITY.COMMUNITY (100)
await registry.register({
id: 'plugin1',
feature: Plugin1,
slashCommands: [{ command: '/test', handler: 'handle' }],
priority: PRIORITY.COMMUNITY,
});
// Plugin 2: PRIORITY.OVERRIDE (2000) - higher priority
await registry.register({
id: 'plugin2',
feature: Plugin2,
slashCommands: [{ command: '/test', handler: 'handle' }],
priority: PRIORITY.OVERRIDE,
});
// ✅ Plugin2 wins, Plugin1's /test is shadowed
// Warning logged: "Command /test from \"plugin1\" (priority 100) is shadowed..."
Event system¶
emit()¶
Emit an event to all subscribed plugins.
Example:
import { CanvasEvent } from '/static/js/plugin-events.js';
const event = new CanvasEvent('node:created', {
nodeId: 'node-123',
nodeType: 'ai',
});
registry.emit('node:created', event);
on()¶
Subscribe to an event (for manual subscription, not typical for plugins).
Example:
Note: Plugins use getEventSubscriptions() instead of calling on() directly.
getEventBus()¶
Get the underlying event emitter.
Example:
Built-in events¶
Events emitted by FeatureRegistry:
| Event | When | Data | Cancellable |
|---|---|---|---|
command:before |
Before slash command executes | { command, args, context } |
Yes |
command:after |
After successful execution | { command, result: 'success' } |
No |
command:error |
On command error | { command, error } |
No |
Example:
registry.on('command:before', (event) => {
console.log('About to execute:', event.data.command);
// Can prevent execution
if (someCondition) {
event.preventDefault();
}
});
registry.on('command:error', (event) => {
console.error('Command failed:', event.data.command, event.data.error);
});
Best practices¶
Plugin registration order¶
Plugins execute event handlers in registration order:
// Plugin A registered first
await registry.register({ id: 'plugin-a', feature: PluginA });
// Plugin B registered second
await registry.register({ id: 'plugin-b', feature: PluginB });
// When event fires:
// 1. PluginA's handler runs
// 2. PluginB's handler runs
Tip: Register extension plugins (Level 3) after base features (Level 2) to ensure proper hook interception.
Priority strategy¶
Choose priorities based on plugin type:
- Built-in features:
PRIORITY.BUILTIN(1000) - Official extensions:
PRIORITY.OFFICIAL(500) - Third-party plugins:
PRIORITY.COMMUNITY(100) - User overrides:
PRIORITY.OVERRIDE(2000)
Example:
// Built-in /search command
await registry.register({
id: 'research',
feature: ResearchFeature,
slashCommands: [{ command: '/search', handler: 'handleSearch' }],
priority: PRIORITY.BUILTIN,
});
// User wants custom search behavior
await registry.register({
id: 'my-search',
feature: MySearchFeature,
slashCommands: [{ command: '/search', handler: 'handleSearch' }],
priority: PRIORITY.OVERRIDE, // Takes precedence
});
Error handling¶
Always handle errors from handleSlashCommand():
try {
const handled = await registry.handleSlashCommand(command, args, context);
if (!handled) {
showToast('Unknown command', 'warning');
}
} catch (error) {
console.error('Command error:', error);
showToast(`Error: ${error.message}`, 'error');
}
Testing¶
Mock FeatureRegistry for testing:
class MockRegistry {
constructor() {
this.features = new Map();
this.events = [];
}
async register(config) {
const instance = new config.feature(mockContext);
this.features.set(config.id, instance);
}
getFeature(id) {
return this.features.get(id);
}
emit(eventName, event) {
this.events.push({ eventName, event });
}
}
Or use PluginTestHarness which includes a real FeatureRegistry.