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.
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),
};
}
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');