FeaturePlugin API reference¶
This document describes the FeaturePlugin base class API for creating Level 2 plugins (feature plugins with slash commands and multi-step workflows).
Class: FeaturePlugin¶
Base class for all feature plugins. Provides dependency injection via AppContext and defines the plugin lifecycle.
Constructor¶
Parameters:
context- AppContext instance providing access to Canvas-Chat APIs
Usage:
class MyFeature extends FeaturePlugin {
constructor(context) {
super(context);
// Access APIs
this.graph = context.graph;
this.canvas = context.canvas;
this.chat = context.chat;
// Initialize feature state
this.myState = {};
}
}
Note: Always call super(context) before accessing context properties.
Lifecycle hooks¶
onLoad()¶
Called when the plugin is registered and loaded. Use for initialization.
When it's called:
- After plugin registration via
FeatureRegistry.register() - Before any slash commands are routed to the plugin
- After event subscriptions are registered
Common uses:
- Load saved state from storage
- Initialize resources
- Set up timers or intervals
- Log plugin loading
Example:
async onLoad() {
console.log('[MyFeature] Loaded');
// Load saved state
const savedData = this.storage.getItem('my-feature-state');
if (savedData) {
this.state = JSON.parse(savedData);
}
// Initialize resources
this.workers = new Map();
// Log to user
this.showToast?.('MyFeature loaded', 'info');
}
onUnload()¶
Called when the plugin is unregistered. Use for cleanup.
When it's called:
- Via
FeatureRegistry.unregister(pluginId) - Before plugin removal from registry
- After event subscriptions are removed
Common uses:
- Save state to storage
- Abort ongoing operations
- Clear timers/intervals
- Release resources
Example:
async onUnload() {
console.log('[MyFeature] Unloaded');
// Save state
this.storage.setItem('my-feature-state', JSON.stringify(this.state));
// Abort operations
for (const [id, worker] of this.workers.entries()) {
worker.abort();
}
this.workers.clear();
// Log to user
this.showToast?.('MyFeature unloaded', 'info');
}
Event subscriptions¶
getEventSubscriptions()¶
Returns an object mapping event names to handler functions for feature registry events.
Returns:
Object where:
- Key: Event name (string)
- Value: Handler function (receives
CanvasEventorCancellableEvent)
Example:
getEventSubscriptions() {
return {
'node:created': this.onNodeCreated.bind(this),
'node:deleted': this.onNodeDeleted.bind(this),
'selfheal:before': this.onSelfHealBefore.bind(this),
};
}
getCanvasEventHandlers()¶
Returns an object mapping canvas event names to handler functions. These handlers are automatically registered on the canvas when the plugin loads and unregistered when the plugin unloads.
When to use:
- Your plugin creates custom node types that emit canvas events (e.g.,
canvas.emit('myCustomEvent', nodeId, ...args)) - You need to handle interactions with custom nodes (clicks, votes, etc.)
- You want to keep event handling logic within your plugin (self-contained)
Returns:
Object where:
- Key: Canvas event name (string)
- Value: Handler function (receives event arguments directly, not wrapped in an event object)
Example:
getCanvasEventHandlers() {
return {
'pollVote': this.handlePollVote.bind(this),
'pollAddOption': this.handlePollAddOption.bind(this),
'pollResetVotes': this.handlePollResetVotes.bind(this),
};
}
handlePollVote(nodeId, optionIndex) {
const node = this.graph.getNode(nodeId);
// Update votes...
this.graph.updateNode(nodeId, { votes: node.votes });
this.canvas.renderNode(node);
}
Note: Canvas event handlers receive arguments directly from canvas.emit(), unlike feature registry events which wrap data in event objects.
onNodeCreated(event) {
const { nodeId, nodeType } = event.data;
console.log('Node created:', nodeId, nodeType);
}
onSelfHealBefore(event) {
// Can prevent self-healing
if (event.data.attemptNum > 2) {
event.preventDefault();
console.log('Prevented self-healing after 2 attempts');
}
}
Available events:
See Extension Hooks Reference for complete event listing.
Event ordering:
- Events fire in plugin registration order (first registered, first called)
- Multiple plugins can subscribe to the same event
- First plugin to call
preventDefault()wins (onCancellableEvent)
Helper methods¶
showToast()¶
Display a toast notification to the user.
Parameters:
message- Text to displaytype- One of:'info','success','warning','error'
Example:
this.showToast('Operation completed', 'success');
this.showToast('Node not found', 'error');
this.showToast('Processing...', 'info');
Note: showToast may be undefined in test environments. Use optional chaining:
emit()¶
Emit a custom event to the event bus.
Parameters:
eventName- Unique event identifierevent- CanvasEvent or CancellableEvent instance
Example:
import { CanvasEvent } from '/static/js/plugin-events.js';
// Emit simple event
this.emit(
'myfeature:started',
new CanvasEvent('myfeature:started', {
featureId: this.id,
timestamp: Date.now(),
})
);
// Emit cancellable event
import { CancellableEvent } from '/static/js/plugin-events.js';
const event = new CancellableEvent('myfeature:before-action', {
action: 'delete',
nodeId: 'node-123',
});
this.emit('myfeature:before-action', event);
if (event.defaultPrevented) {
console.log('Action cancelled by another plugin');
return;
}
Inherited properties (via AppContext)¶
All properties from AppContext are available via this.context or directly:
Graph API¶
Graph data structure managing nodes and edges.
Common methods:
// Nodes
this.graph.addNode(node);
this.graph.getNode(nodeId);
this.graph.updateNode(nodeId, updates);
this.graph.deleteNode(nodeId);
this.graph.getAllNodes();
this.graph.getLeafNodes();
// Edges
this.graph.addEdge(edge);
this.graph.getEdge(edgeId);
this.graph.deleteEdge(edgeId);
this.graph.getEdgesForNode(nodeId);
// Traversal
this.graph.resolveContext(nodeIds); // Get conversation context
this.graph.getVisibleSubtree(nodeId); // Get expanded descendants
// Positioning
this.graph.autoPosition(parentIds); // Calculate position for new node
Canvas API¶
Visual canvas for rendering and interaction.
Common methods:
// Rendering
this.canvas.renderNode(node);
this.canvas.removeNode(nodeId);
this.canvas.updateNodeContent(nodeId, content, isStreaming);
// Selection
this.canvas.getSelectedNodeIds(); // Returns: string[]
this.canvas.clearSelection();
// Viewport
this.canvas.centerOnAnimated(x, y, duration);
this.canvas.panToNodeAnimated(nodeId);
// Streaming controls
this.canvas.showStopButton(nodeId);
this.canvas.hideStopButton(nodeId);
this.canvas.showContinueButton(nodeId);
this.canvas.hideContinueButton(nodeId);
// Edges
this.canvas.renderEdge(edge, fromPosition, toPosition);
this.canvas.removeEdge(edgeId);
Chat API¶
LLM communication interface.
Common methods:
// Get API credentials
const apiKey = this.chat.getApiKeyForModel(model);
const baseUrl = this.chat.getBaseUrlForModel(model);
// Send message (streaming)
await this.chat.sendMessage(
messages, // Array of {role, content}
model, // Model ID
onChunk, // (chunk, fullContent) => void
onDone, // () => void
onError, // (error) => void
signal // AbortSignal (optional)
);
// Summarize (non-streaming)
const summary = await this.chat.summarize(messages, model);
// Token estimation
const tokens = this.chat.estimateTokens(text, model);
Storage API¶
LocalStorage wrapper for persistence.
Common methods:
// Key-value storage
this.storage.setItem(key, value);
const value = this.storage.getItem(key);
this.storage.removeItem(key);
// Session management
this.storage.saveSession(session);
const session = this.storage.getSession(sessionId);
// API keys (read-only)
const apiKeys = this.storage.getApiKeys();
const exaKey = this.storage.getExaApiKey();
Model picker¶
UI element for model selection.
Usage:
const model = this.modelPicker.value; // Current selected model
console.log('Using model:', model); // e.g., 'openai/gpt-4'
Feature registry¶
Plugin registry for accessing other plugins.
Common methods:
// Get other plugins
const otherPlugin = this.featureRegistry.getFeature('other-plugin-id');
// Emit events
this.featureRegistry.emit('my-event', event);
// Get registered commands
const commands = this.featureRegistry.getSlashCommands();
App instance¶
Main application instance (use sparingly, prefer specific APIs).
Common methods:
// Session management
this.app.saveSession();
// Modals (prefer custom modals in your plugin)
this.app.modalManager.showModal(modalId);
Slash command handlers¶
Slash command handlers are registered via FeatureRegistry and called when users type the command.
Handler signature¶
Parameters:
command- Full command string (e.g.,'/mycommand')args- Everything after the command (e.g.,'arg1 arg2')context- Object with:text- Selected text or node content (string)
Example:
async handleMyCommand(command, args, context) {
console.log('Command:', command); // '/mycommand'
console.log('Args:', args); // 'arg1 arg2'
console.log('Context:', context.text); // Selected text or ''
// Parse args
const parts = args.split(' ');
const firstArg = parts[0];
// Use context
if (context.text) {
console.log('User selected:', context.text);
}
// Implement command logic
await this.doSomething(firstArg);
}
Best practices¶
State management¶
Do:
constructor(context) {
super(context);
// Per-instance state for concurrent operations
this.operations = new Map();
// Feature-wide state
this.config = {};
}
Don't:
constructor(context) {
super(context);
// Single variables for concurrent operations
this.currentOperation = null; // BAD: only one operation can run
}
Resource cleanup¶
Always clean up in onUnload():
async onUnload() {
// Abort ongoing operations
for (const [id, op] of this.operations.entries()) {
op.abortController.abort();
}
this.operations.clear();
// Clear intervals
if (this.intervalId) {
clearInterval(this.intervalId);
}
// Save state
this.storage.setItem('state', JSON.stringify(this.state));
}
Error handling¶
Always handle errors in async operations:
async handleCommand(command, args, context) {
try {
await this.doAsyncWork(args);
this.showToast('Success!', 'success');
} catch (error) {
console.error('Command failed:', error);
this.showToast(`Error: ${error.message}`, 'error');
}
}
Binding event handlers¶
Bind methods correctly:
getEventSubscriptions() {
return {
// Correct: bind this
'my-event': this.onMyEvent.bind(this),
// Wrong: loses 'this' context
// 'my-event': this.onMyEvent,
};
}
Testing¶
Test plugins with PluginTestHarness:
import { PluginTestHarness } from '/static/js/plugin-test-harness.js';
const harness = new PluginTestHarness();
await harness.loadPlugin({
id: 'my-feature',
feature: MyFeature,
slashCommands: [{ command: '/mycommand', handler: 'handleMyCommand' }],
});
// Test command
await harness.executeCommand('/mycommand', 'arg', { text: 'context' });
// Test event
const event = new CanvasEvent('test-event', { data: 'value' });
await harness.emitEvent('test-event', event);
// Verify
const nodes = harness.mockCanvas.getRenderedNodes();
console.assert(nodes.length === 1);
// Cleanup
await harness.unloadPlugin('my-feature');