Skip to main content

Plugin API Reference

Complete reference for the PluginContext object passed to your plugin factory. Every method available for extending Voiden is documented here.


Plugin Entry Point

import type { CorePluginContext } from '@voiden/sdk/ui';
import manifest from '../manifest.json';

export default function createMyPlugin(context: CorePluginContext) {
return {
onload: async () => {
// Register everything here
},

onunload: async () => {
// Cancel subscriptions, clean up resources
},

metadata: manifest,
};
}
HookWhen it runs
onloadCalled once when the plugin activates — register all features here
onunloadCalled on disable or app close — cancel any subscriptions made in onload
metadataPass manifest directly — used for Extensions browser display
info

Import CorePluginContext from @voiden/sdk/ui, not @voiden/sdk. The /ui path provides the full extended context including pipeline hooks, UI utilities, and all the APIs documented here.


Slash Commands

addVoidenSlashGroup(group)

Register a group of slash commands that appear in the / menu.

context.addVoidenSlashGroup({
name: "my-group",
title: "My Commands",
commands: [
{
name: "insert-widget",
label: "Insert Widget",
description: "Inserts a widget block",
slash: "/widget",
icon: "Box",
action: (editor) => {
editor.commands.insertContent({ type: "paragraph", content: [{ type: "text", text: "Widget!" }] });
},
},
],
});

SlashCommandDefinition:

FieldTypeRequiredDescription
namestringYesUnique identifier
labelstringYesDisplay text in the menu
descriptionstringYesDescription shown below the label
slashstringYesTrigger text (e.g. /widget)
iconstringNoLucide icon name
action(editor) => voidYesHandler called when selected
aliasesstring[]NoAlternative trigger text
singletonbooleanNoAllow only one instance in a document
compareKeysstring[]NoKeys to compare for singleton detection
isEnabled(editor) => booleanNoDynamic enable/disable
shouldBeHidden(editor) => booleanNoDynamic visibility

addVoidenSlashCommand(command)

Register a single slash command without a group.

context.addVoidenSlashCommand({
name: "greet",
label: "Say Hello",
description: "Insert a greeting",
slash: "/hello",
action: (editor) => editor.commands.insertContent("Hello!"),
});

Editor Extensions

registerVoidenExtension(extension)

Register a TipTap node, mark, or plugin with the Voiden editor.

import { Node } from "@tiptap/core";

const MyNode = Node.create({
name: "myWidget",
group: "block",
content: "inline*",
parseHTML() { return [{ tag: "div[data-type=my-widget]" }]; },
renderHTML({ HTMLAttributes }) { return ["div", { "data-type": "my-widget", ...HTMLAttributes }, 0]; },
});

context.registerVoidenExtension(MyNode);

unregisterVoidenExtension(name)

Remove a previously registered TipTap extension.

context.unregisterVoidenExtension("myWidget");

registerCodemirrorExtension(extension)

Register a CodeMirror extension with the code editor (autocomplete, linting, syntax highlighting).

context.registerCodemirrorExtension(myCodemirrorPlugin());

unregisterCodemirrorExtension(extension)

Remove a previously registered CodeMirror extension.


UI Registration

registerSidebarTab(side, tab)

Add a tab to the left or right sidebar.

context.registerSidebarTab("right", {
id: "my-sidebar",
title: "My Panel",
icon: "Zap",
component: MySidebarComponent,
badge: 3, // optional — shows a badge on the tab
});
FieldTypeRequiredDescription
idstringYesUnique tab identifier
titlestringYesDisplay title
iconstringNoLucide icon name
componentReact.ComponentTypeYesReact component to render
badgestring | numberNoBadge content shown on the tab

registerPanel(panelId, panel)

Register a panel component that can be opened as a tab.

context.registerPanel("main", {
id: "my-panel",
title: "My Panel",
component: MyPanelComponent,
});

addTab(panelId, tab)

Open a tab in a panel. Provide the component inline or after registerPanel.

context.addTab("main", {
id: "my-tab",
icon: "FileText",
title: "My Tab",
props: {},
component: MyComponent,
});

registerEditorAction(action)

Add a button to the code editor toolbar.

context.registerEditorAction({
id: "my-action",
component: MyActionButton,
predicate: (doc) => doc.title?.endsWith(".json"),
});

registerStatusBarItem(item)

Add an item to the bottom status bar.

context.registerStatusBarItem({
id: "my-status",
icon: "Activity", // string or React.ComponentType
label: "My Plugin",
tooltip: "Click to open",
position: "left",
onClick: () => { /* open a tab, toggle panel, etc. */ },
});
FieldTypeRequiredDescription
idstringYesUnique identifier
iconstring | React.ComponentTypeYesLucide icon name or component
labelstringNoText label next to the icon
tooltipstringYesHover tooltip
position'left' | 'right'YesWhich side of the status bar
onClick() => voidYesClick handler

registerTopBarItem(item)

Inject an icon button into the top navigation bar.

import { Rocket } from "lucide-react";

context.registerTopBarItem({
id: "my-topbar-btn",
icon: Rocket, // React.ComponentType — import from lucide-react
tooltip: "Launch",
position: "right", // 'left' | 'right', defaults to 'right'
onClick: () => { /* ... */ },
});
FieldTypeRequiredDescription
idstringYesUnique identifier
iconReact.ComponentType<any>YesIcon component (e.g. from lucide-react)
tooltipstringNoHover tooltip
position'left' | 'right'NoSide of the nav bar (default: 'right')
onClick() => voidYesClick handler
info

registerTopBarItem is also available as context.ui.registerTopBarItem — both point to the same API.


Command Palette

Requires manifest permission: "commandPalette"

registerCommand(cmd)

Register an entry in the command palette (⌘⇧P).

import { Play } from "lucide-react";

context.registerCommand({
id: "my-plugin.run-all",
label: "My Plugin: Run All Tests",
description: "Execute all .void files in the project",
icon: Play,
shortcut: "⌘⇧T",
when: () => true, // optional — hide the command when returns false
action: () => { /* ... */ },
});
FieldTypeRequiredDescription
idstringYesUnique identifier (e.g. "my-plugin.action")
labelstringYesDisplay label in the palette
descriptionstringNoSubtitle shown below the label
iconReact.ComponentTypeNoIcon component
shortcutstringNoKeyboard shortcut hint (display only — not bound automatically)
when() => booleanNoCommand is hidden when this returns false
action() => voidYesExecuted when the command is selected

Context Menus

Requires manifest permission: "contextMenus"

registerContextMenu(item)

Inject an item into a right-click context menu surface.

import { Copy } from "lucide-react";

context.registerContextMenu({
id: "my-plugin.copy-request",
label: "Copy as cURL",
icon: Copy,
surface: "tab", // 'tab' | 'file' | 'block'
when: (target) => !!target.filePath?.endsWith(".void"),
action: (target) => {
console.log("Right-clicked:", target);
},
});
FieldTypeRequiredDescription
idstringYesUnique identifier
labelstringYesDisplay label
iconReact.ComponentTypeNoIcon component
surface'tab' | 'file' | 'block'YesWhich context menu this item appears in
when(target: any) => booleanNoItem is hidden when this returns false
action(target: any) => voidYesCalled with the right-clicked target object

Events

Requires manifest permission: "events"

context.events.on(event, callback)

Subscribe to workspace lifecycle events. Always store the returned unsubscribe function and call it in onunload.

const cleanupFns: Array<() => void> = [];

context.events.on('tab:changed', ({ tabId, title, type }) => {
console.log('Tab changed to:', title);
});

context.events.on('file:saved', ({ filePath, tabId }) => {
console.log('Saved:', filePath);
});

context.events.on('project:changed', ({ projectPath }) => {
console.log('Project switched to:', projectPath);
});

context.events.on('environment:changed', ({ envPath }) => {
console.log('Active environment changed:', envPath);
});

context.events.on('request:sent', ({ request }) => {
console.log('Request sent:', request.url);
});

context.events.on('response:received', ({ response }) => {
console.log('Response:', response.status);
});

// In onunload — cancel all subscriptions:
cleanupFns.forEach(fn => fn());

Supported events:

EventPayloadDescription
'tab:changed'{ tabId, title, type }A different document tab became active
'file:saved'{ filePath, tabId }A file was saved
'project:changed'{ projectPath }The active project folder changed
'environment:changed'{ envPath }The active environment file changed
'request:sent'{ request }A request was sent
'response:received'{ response }A response was received

File System

Requires manifest permission: "filesystem"

All paths are relative to the active project root. There is no access to paths outside the open project.

context.fs

// Read a file
const content = await context.fs.read('config.json');

// Write to a file (creates if missing)
await context.fs.write('output.txt', 'hello world');

// Create a new file with optional initial content
await context.fs.create('notes/new.md', '# Notes\n');

// Create a directory (and any missing parents)
await context.fs.createDirectory('reports/2026');

// Delete a file or directory
await context.fs.delete('temp.txt');

// Move a file (creates destination directory if missing)
await context.fs.move('old/path.void', 'new/path.void');

// Check if a path exists
const exists = await context.fs.exists('config.json');

// List entries at a path (defaults to project root)
const entries = await context.fs.list('src');
// Returns: [{ name: 'plugin.ts', path: 'src/plugin.ts', type: 'file' }, ...]
MethodSignatureDescription
read(path) => Promise<string>Read a file's text content
write(path, content) => Promise<void>Write text to a file (creates if missing)
create(path, content?) => Promise<void>Create a new file with optional initial content
createDirectory(path) => Promise<void>Create a directory and any missing parents
delete(path) => Promise<void>Delete a file or directory
move(fromPath, toPath) => Promise<void>Move a file; creates the destination directory if missing
exists(path) => Promise<boolean>Return true if the path exists
list(path?) => Promise<Entry[]>List entries at a path; defaults to project root

Settings

Requires manifest permission: "settings"

context.settings

Persist and retrieve plugin configuration values (plain JSON, stored per-plugin).

// Get a value
const theme = await context.settings.get<string>('theme');

// Set a value
await context.settings.set('theme', 'dark');

// Delete a key
await context.settings.delete('theme');

// Subscribe to changes — returns an unsubscribe function
const unsub = context.settings.onChange((key, value) => {
console.log(`Setting changed: ${key} = ${value}`);
});
// Call unsub() in onunload

context.ui.registerSettings(section)

Register a settings section in the Voiden Settings panel. The host renders the fields using its own UI primitives — no custom React components needed.

import { Sliders } from "lucide-react";

context.ui.registerSettings({
id: "my-plugin-settings",
title: "My Plugin",
icon: Sliders,
fields: [
{
type: "toggle",
key: "enabled",
label: "Enable feature",
description: "Turn the feature on or off",
defaultValue: true,
},
{
type: "text",
key: "apiKey",
label: "API Key",
placeholder: "sk-...",
},
{
type: "number",
key: "timeout",
label: "Timeout (ms)",
defaultValue: 5000,
min: 0,
max: 30000,
},
{
type: "select",
key: "mode",
label: "Mode",
options: [
{ label: "Fast", value: "fast" },
{ label: "Accurate", value: "accurate" },
],
defaultValue: "fast",
},
],
});

Supported field types: text, number, select, toggle

Each field accepts key (used with context.settings.get/set), label, and description?. Values are persisted automatically via the settings API.


Theme

context.theme

Ready-to-use Tailwind class tokens that map to the active Voiden theme. Use these in your plugin's JSX instead of hard-coding colour values so your UI automatically follows light/dark mode.

function MyPanel() {
const { theme } = usePluginContext(); // or receive it as a prop

return (
<div className={`${context.theme.bg.surface} ${context.theme.text.primary} p-4`}>
<h2 className={`${context.theme.text.ui} font-medium mb-2`}>My Plugin</h2>

<button className={`${context.theme.button.primary} px-3 py-1 rounded`}>
Send
</button>

<span className={`${context.theme.http.get} ${context.theme.http.getBg} px-2 py-0.5 rounded text-xs`}>
GET
</span>

<p className={context.theme.status.successText}>Passed</p>
</div>
);
}

Token groups:

GroupTokensExample use
theme.bgprimary, surface, panel, overlay, active, hover, accent, altContainer backgrounds
theme.textprimary, muted, ui, light, accent, altText colours
theme.borderDEFAULT, light, subtle, lineBorders and dividers
theme.buttonprimary, primaryHover, secondary, secondaryFg, danger, dangerHoverButton backgrounds
theme.statussuccessBg/Text, errorBg/Text, warningBg/Text, infoBg/TextStatus indicators
theme.httpget, post, put, patch, delete, head, options + *Bg variantsHTTP method badges
theme.iconprimary, secondary, success, error, warning, infoIcon colours
theme.interactiveactive, hoverInteractive state backgrounds
theme.codebg, fg, selection, line, gutterCode display
theme.menubg, hover, separatorDropdown/context menus
theme.badgecoreBg/Fg/Border, officialBg/Fg/Border, communityBg/Fg/BorderExtension type badges
theme.testpassedBg/Text, failedBg/TextAssertion result chips

THEME_CLASSES can also be imported directly for use outside a plugin context:

import { THEME_CLASSES } from '@voiden/sdk/ui';

Request Pipeline

onBuildRequest(handler)

Modify the request object before it is sent. Multiple handlers run in registration order.

context.onBuildRequest(async (request, editor) => {
request.headers.push({ key: "X-Custom", value: "my-value", enabled: true });
return request;
});
warning

Never expand environment variables ({{VARIABLE}}) in your handler. Voiden handles variable substitution securely in a separate stage.

onProcessResponse(handler)

Run logic after a response is received.

context.onProcessResponse(async (response) => {
console.log(`Status: ${response.status}`);
});

registerResponseSection(section)

Register a section to display in the response panel.

context.registerResponseSection({
id: "my-results",
name: "My Results",
component: MyResultsComponent,
order: 10,
});

Project & File Access

context.project

// Get the active project path
const projectPath = await context.project.getActiveProject();

// Get all .void files in the project
const files = await context.project.getVoidFiles();

// Open a file in the editor
await context.project.openFile("requests/users.void");

// Create a file
await context.project.createFile("requests/new.void", "# New Request\n");

// Create a folder
await context.project.createFolder("requests/subfolder");

// Import a cURL command as a new document tab
await context.project.importCurl("My Request", "curl -X GET https://api.example.com/users");

// Get the active editor instance
const editor = context.project.getActiveEditor("voiden");

Helpers

exposeHelpers(helpers)

Expose utility functions for other plugins to consume.

context.exposeHelpers({
parseJSON: (text: string) => JSON.parse(text),
formatDate: (date: Date) => date.toISOString(),
});

context.helpers.from(pluginId)

Get helpers exposed by another plugin.

const fakerHelpers = context.helpers.from("voiden-faker");
if (fakerHelpers) {
const value = fakerHelpers.generate("name");
}

context.helpers.parseVoid(markdown)

Parse a .void markdown document into Voiden's internal format.

const doc = context.helpers.parseVoid("# My Document\nSome content");
info

Helpers must be pure functions — no side effects, no network calls, no file access.


Paste Handling

context.paste.registerBlockOwner(handler)

Claim ownership of a block type for paste handling. Only one owner per block type.

context.paste.registerBlockOwner({
blockType: "my-block",
allowExtensions: true,
handlePasteInside: (text, html, node, view) => false,
processBlock: (block) => block,
});

context.paste.registerPatternHandler(handler)

Handle specific paste patterns (e.g. cURL, URLs).

context.paste.registerPatternHandler({
canHandle: (text) => text.startsWith("curl "),
handle: (text, html, view) => {
// Parse and insert
return true;
},
});

context.paste.registerBlockExtension(extension)

Extend a block type owned by another plugin.

context.paste.registerBlockExtension({
blockType: "request",
extendBlock: (block, context) => block,
});

Tab Management

openVoidenTab(title, content, options?)

Open a new Voiden editor tab with JSON document content.

await context.openVoidenTab("Preview", documentJSON, { readOnly: true });

registerLinkableNodeTypes(nodeTypes)

Register node types that can be linked/referenced across files.

context.registerLinkableNodeTypes(["my-block", "my-output"]);

registerNodeDisplayNames(displayNames)

Register human-readable display names for node types shown in the UI.

context.registerNodeDisplayNames({
"my-block": "My Widget",
"my-output": "Widget Output",
});

registerTableSuggestions(tableType, suggestions)

Register autocomplete suggestions for table cell blocks. Maps column indices to suggestion items.

context.registerTableSuggestions("headers-table", {
0: [
{ label: "Content-Type", description: "Media type of the request body" },
{ label: "Authorization", description: "Auth credentials" },
{ label: "Accept", description: "Acceptable response media types" },
],
1: [
{ label: "application/json" },
{ label: "text/plain" },
],
});

Help Commands

registerHelpCommand(cmd)

Register a command in the Voiden help panel.

context.registerHelpCommand({
id: "my-plugin.help",
label: "My Plugin: How to use",
description: "Learn how to use My Plugin",
component: MyHelpContent,
});

UI Utilities

context.ui

// Panel controls
context.ui.openRightPanel();
context.ui.closeRightPanel();
context.ui.toggleRightPanel();
context.ui.openBottomPanel();
context.ui.closeBottomPanel();

// Open a specific sidebar tab
context.ui.openRightSidebarTab("my-sidebar");

// Show a toast notification
context.ui.showToast("Saved!", "success"); // 'info' | 'success' | 'warning' | 'error'

// Prose styling classes that follow the active theme
const classes = context.ui.getProseClasses();

context.ui.components

Shared UI components for use inside your React components:

ComponentDescription
CodeEditorCodeMirror-based code editor
Table, TableBody, TableRow, TableCellStyled table primitives
NodeViewWrapperTipTap node view wrapper
RequestBlockHeaderRequest block header with link/unlink support
<context.ui.components.CodeEditor
lang="json"
value='{"key": "value"}'
onChange={(v) => console.log(v)}
readOnly={false}
/>

Full PluginContext Type Reference

interface PluginContext {
// Slash commands
addVoidenSlashCommand(command: SlashCommandDefinition): void;
addVoidenSlashGroup(group: SlashCommandGroup): void;
getVoidenSlashGroups(): SlashCommandGroup[];

// Editor extensions
registerVoidenExtension(extension: any): void;
unregisterVoidenExtension(name: string): void;
registerCodemirrorExtension(extension: any): void;
unregisterCodemirrorExtension(extension: any): void;

// UI registration
registerSidebarTab(side: 'left' | 'right', tab: TabDefinition): void;
registerPanel(panelId: string, panel: TabDefinition): void;
addTab(tabId: string, tab: Panel): void;
registerEditorAction(action: EditorAction): void;
registerStatusBarItem(item: StatusBarItem): void;
registerTopBarItem(item: PluginTopBarItem): void;
registerHelpCommand(cmd: PluginHelpCommand): void;

// Command palette (requires "commandPalette" permission)
registerCommand(cmd: PluginCommand): void;

// Context menus (requires "contextMenus" permission)
registerContextMenu(item: PluginContextMenuItem): void;

// Events (requires "events" permission)
events: {
on(event: string, callback: (data: any) => void): () => void;
};

// File system (requires "filesystem" permission)
fs: {
read(path: string): Promise<string>;
write(path: string, content: string): Promise<void>;
create(path: string, content?: string): Promise<void>;
createDirectory(path: string): Promise<void>;
delete(path: string): Promise<void>;
move(fromPath: string, toPath: string): Promise<void>;
exists(path: string): Promise<boolean>;
list(path?: string): Promise<Array<{ name: string; path: string; type: 'file' | 'directory' }>>;
};

// Settings (requires "settings" permission)
settings: {
get<T = any>(key: string): Promise<T | undefined>;
set<T = any>(key: string, value: T): Promise<void>;
delete(key: string): Promise<void>;
onChange(callback: (key: string, value: any) => void): () => void;
};

// Theme tokens
theme: ThemeClasses;

// Project & file access
project: {
getActiveEditor(type: 'code' | 'voiden'): any;
getActiveProject(): Promise<string>;
getVoidFiles(): Promise<DocumentTab[]>;
createFile(filePath: string, content: string): Promise<void>;
createFolder(folderPath: string): Promise<void>;
openFile(relativePath: string): Promise<void>;
importCurl(title: string, curlString: string): Promise<void>;
};

// Helpers
exposeHelpers(helpers: Record<string, (...args: any[]) => any>): void;
helpers: {
parseVoid(markdown?: string): any;
from<T>(pluginId: string): T | undefined;
};

// Request pipeline
onBuildRequest(handler: (request: any, editor: Editor) => any): void;
onProcessResponse(handler: (response: any) => void): void;
registerResponseSection(section: ResponseSection): void;

// Paste handling
paste: {
registerBlockOwner(handler: BlockPasteHandler): void;
registerBlockExtension(extension: BlockExtension): void;
registerPatternHandler(handler: PatternHandler): void;
};

// Tab & node management
openVoidenTab(title: string, content: any, options?: { readOnly?: boolean }): Promise<void>;
registerLinkableNodeTypes(nodeTypes: string[]): void;
registerNodeDisplayNames(displayNames: Record<string, string>): void;
registerTableSuggestions(tableType: string, suggestions: TableSuggestionsConfig): void;

// UI utilities
ui: {
getProseClasses(): string;
openRightPanel(): void;
closeRightPanel(): void;
toggleRightPanel(): void;
openBottomPanel(): void;
closeBottomPanel(): void;
openRightSidebarTab(tabId: string): void;
showToast(message: string, type?: 'info' | 'success' | 'warning' | 'error'): void;
registerTopBarItem(item: PluginTopBarItem): void;
registerSettings(section: PluginSettingsSection): void;
components: UIComponents;
hooks: { useSendRestRequest(editor: any): { refetch(): void; isLoading: boolean; error: any; data: any; cancelRequest(): void } };
};
}