Power SDK reference
The window.FeatherPanel host API (version 2) for plugin frontends: 13 events, 10 cancellable actions, the payload of each, what the host reads back, and examples.
Last synced from FeatherPanel 15b94e9 on .
On this page
Code that runs in the panel window (your plugin's Frontend/index.js, see Frontend) gets window.FeatherPanel. Plugin pages in iframes reach the same bus through postMessage. Events are notifications: the panel tells you something happened. Actions are hooks that run before the panel does something, and can change or cancel it. The tables below are read from the panel source, so the payload keys are the ones the panel actually sends.
fp:files:selection, fp:files:upload. Listening to them is harmless but nothing will arrive.Host API
window.FeatherPanel.version is 2. The api methods call plain axios with the URL you pass (use the full path, for example /api/user/session), so same-origin requests carry the user's session cookie. They resolve to the response body, run the fp:api:request action first and emit fp:api:error when the request fails. navigate() runs fp:nav:push first. on, register, contribute and subscribe return a function that removes what they added.
interface FeatherPanelHostApi {
version: number;
events: {
on: (event: string, handler: EventHandler) => () => void;
once: (event: string, handler: EventHandler) => () => void;
off: (event: string, handler: EventHandler) => void;
emit: (event: string, payload?: unknown) => void;
};
actions: {
register: (actionId: string, registration: ActionRegistration) => () => void;
unregister: (actionId: string, handlerId: string) => void;
run: (actionId: string, ctx?: Record<string, unknown>) => Promise<ActionContext>;
};
theme: {
getMode: () => 'light' | 'dark';
getAccent: () => string;
getPackId: () => string;
subscribe: (listener: () => void) => () => void;
};
api: {
get: <T = unknown>(url: string, config?: object) => Promise<T>;
post: <T = unknown>(url: string, data?: unknown, config?: object) => Promise<T>;
put: <T = unknown>(url: string, data?: unknown, config?: object) => Promise<T>;
delete: <T = unknown>(url: string, config?: object) => Promise<T>;
};
toast: {
success: (message: string) => void;
error: (message: string) => void;
info: (message: string) => void;
message: (message: string) => void;
};
navigate: (path: string) => void;
context: {
getPathname: () => string;
getUser: () => unknown;
getUiPackId: () => string;
subscribe: (listener: () => void) => () => void;
};
ui: {
modal: {
register: (def: PluginModalDefinition) => () => void;
open: (id: string, props?: Record<string, unknown>) => void;
close: () => void;
};
};
search: {
contribute: (provider: PluginSearchProvider) => () => void;
query: (q: string) => Promise<PluginSearchResult[]>;
};
shortcuts: {
register: (shortcut: PluginShortcut) => () => void;
};
}Events
Subscribe with events.on(id, handler). The handler gets one payload object with these keys.
| Event | Payload keys | Sent from |
|---|---|---|
fp:host:readyFP_EVENTS.HOST_READY | version | |
fp:route:changeFP_EVENTS.ROUTE_CHANGE | pathname, prev | |
fp:page:mountFP_EVENTS.PAGE_MOUNT | pathname, area | |
fp:page:unmountFP_EVENTS.PAGE_UNMOUNT | pathname | |
fp:theme:changeFP_EVENTS.THEME_CHANGE | theme, accentColor, themePackId | |
fp:uipack:changeFP_EVENTS.UI_PACK_CHANGE | uiPackId | |
fp:session:changeFP_EVENTS.SESSION_CHANGE | user, authenticated | |
fp:server:contextFP_EVENTS.SERVER_CONTEXT | uuidShort, entering, pathname | |
fp:server:power:resultFP_EVENTS.SERVER_POWER_RESULT | uuid, action, ok, cancelled, reason, error | |
fp:server:console:readyFP_EVENTS.SERVER_CONSOLE_READY | uuid, uuidShort | |
fp:files:savedFP_EVENTS.FILES_SAVED | uuid, path | |
fp:files:selectionFP_EVENTS.FILES_SELECTION | - | not sent |
fp:api:errorFP_EVENTS.API_ERROR | method, url, error |
Actions
Register with actions.register(id, { id, priority, handler }). "Cancellable" means the panel checks ctx.cancelled after the chain and stops. "Reads back" lists the context fields the panel uses after the chain, so changing them changes what the panel does.
| Action | Context keys | Cancellable | Reads back |
|---|---|---|---|
fp:server:powerServerConsolePage.tsx:549 | action, uuid, uuidShort | yes | action |
fp:files:savefiles-api.ts:267 | uuid, path, content | yes | path, content |
fp:files:uploadnot run by the panel | - | - | - |
fp:nav:pushFeatherPanelHost.tsx:127, host-api.ts:116 | path | yes | path |
fp:form:submitdata-fp-action.ts:86 | formId, action, method, data, source | yes | submitNative |
fp:ui:confirmServerConsolePage.tsx:567 | kind, uuid, message | yes | - |
fp:api:requesthost-api.ts:37 | method, url, data, config | yes | url, data, config |
fp:search:queryuseGlobalSearchItems.ts:685 | query | yes | query |
fp:shortcut:invokePluginShortcutHost.tsx:50 | shortcutId, combo, actionId | yes | - |
fp:data:actiondata-fp-action.ts:56 | actionId | no | - |
fp:form:submit only runs for forms that carry data-fp-action="fp:form:submit"; a form with any other data-fp-action value runs that action instead. A cancelled fp:ui:confirm stops a server kill. fp:data:action runs after every data-fp-action click that was not cancelled, with the action id and the element's payload, for plugins that want to observe clicks.
How actions run
- Handlers run highest
priorityfirst (default0). Registering the same handleridagain replaces the old one. - Each handler receives
(ctx, next). Callawait next()to run the rest of the chain. A handler that returns without callingnext()stops the chain without cancelling the action. ctx.cancel(reason)setsctx.cancelledandctx.cancelReason(default'cancelled'). Later handlers do not run.- The context object is shared, so a handler can change fields for later handlers and for the panel.
actions.run(id, ctx)runs a chain yourself, so plugins can define their own actions for other plugins to hook.
Actions from HTML
The panel watches the page for clicks on elements with data-fp-action and for submits of forms with data-fp-action. It prevents the default behaviour and runs the named action:
| Trigger | Context | After the chain |
|---|---|---|
[data-fp-action] click | ...payload, elementId, href, source | Unless cancelled, runs fp:data:action and then navigates to ctx.href (or the element's href) if there is one. |
form[data-fp-action] submit | formId, action, method, data, source | Unless cancelled, submits the form natively when ctx.submitNative is true. |
data-fp-payload must be JSON; anything else arrives as { raw: '...' }.
Examples
Confirm a power action
// Frontend/index.js
const fp = window.FeatherPanel;
// Ask before a server is stopped. ctx.action is 'start', 'stop', 'restart' or 'kill'.
fp?.actions.register('fp:server:power', {
id: 'myplugin-confirm-stop',
priority: 10,
handler: async (ctx, next) => {
if (ctx.action === 'stop' && !window.confirm('Stop this server?')) {
ctx.cancel('Stop cancelled');
return;
}
await next();
},
});
// Every power request reports back, including cancelled ones.
fp?.events.on('fp:server:power:result', (p) => {
if (p.cancelled) console.info('[myplugin] power action cancelled:', p.reason);
});Change a file before it is saved
// Normalise line endings before a file is written. The host reads ctx.path and ctx.content back.
window.FeatherPanel?.actions.register('fp:files:save', {
id: 'myplugin-crlf',
handler: async (ctx, next) => {
if (typeof ctx.content === 'string' && ctx.path.endsWith('.properties')) {
ctx.content = ctx.content.replace(/\r\n/g, '\n');
}
await next();
},
});Search, shortcuts and modals
const fp = window.FeatherPanel;
// Results in the panel's global search
fp?.search.contribute({
id: 'myplugin-search',
search: async (query) => [
{ id: 'myplugin-docs', title: 'My plugin settings', description: query, href: '/admin/myplugin', category: 'My plugin' },
],
});
// A keyboard shortcut that runs a direct handler
fp?.shortcuts.register({
id: 'myplugin-open',
combo: 'ctrl+shift+m',
description: 'Open My plugin',
handler: () => fp.navigate('/admin/myplugin'),
});
// A modal that shows a page from your plugin's public folder
fp?.ui.modal.register({ id: 'myplugin-modal', title: 'My plugin', componentUrl: '/components/myplugin/modal.html', size: 'md' });
fp?.ui.modal.open('myplugin-modal');Buttons and forms
<!-- Runs the action "myplugin:refresh" with { scope: 'all', elementId, href, source: 'data-fp-action' } -->
<button id="refresh" data-fp-action="myplugin:refresh" data-fp-payload='{"scope":"all"}'>Refresh</button>
<!-- Runs "myplugin:subscribe" with { formId, action, method, data, source: 'data-fp-form' } -->
<form id="subscribe" data-fp-action="myplugin:subscribe" method="post">
<input name="email" type="email" />
<button type="submit">Subscribe</button>
</form>
<script>
window.FeatherPanel?.actions.register('myplugin:subscribe', {
id: 'myplugin-subscribe',
handler: async (ctx, next) => {
await window.FeatherPanel.api.post('/api/myplugin/subscribe', ctx.data);
ctx.submitNative = false; // true submits the form normally after the chain
await next();
},
});
</script>Types
type ActionContext = Record<string, unknown> & {
/** Set by cancel() - when true, the host must abort the action. */
cancelled?: boolean;
cancelReason?: string;
cancel: (reason?: string) => void;
};
type ActionHandler = (ctx: ActionContext, next: () => Promise<void>) => void | Promise<void>;
type ActionRegistration = {
/** Unique handler id (plugin-scoped). */
id: string;
/** Higher runs earlier. Default 0. */
priority?: number;
handler: ActionHandler;
};
type PluginModalDefinition = {
id: string;
title?: string;
/** Public URL to HTML iframe */
componentUrl?: string;
/** Public URL to ESM React remote */
remoteUrl?: string;
/** Optional width class hint */
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
};
type PluginSearchResult = {
id: string;
title: string;
description?: string;
href?: string;
category?: string;
onSelect?: () => void;
};
type PluginSearchProvider = {
id: string;
/** Sync or async results for the current query */
search: (query: string) => PluginSearchResult[] | Promise<PluginSearchResult[]>;
};
type PluginShortcut = {
id: string;
/** e.g. "ctrl+shift+m" (lowercase, + separated) */
combo: string;
description?: string;
/** Run a named action hook, or a direct handler */
actionId?: string;
handler?: () => void | Promise<void>;
};