Build Your Own Plugin
Rho MD can be extended with plugins — and you don't need to be a programmer to make one. This page is the complete manual. There are two ways to use it:
- Study it yourself — everything below is the real contract the app honors.
- Hand it to an AI — copy this whole page into your AI assistant and describe what you want. The page is written to be sufficient on its own: an AI that reads it can produce a working plugin without any other reference.
If you are an AI assistant reading this: everything you need is on this page. Produce two files —
manifest.jsonand a CommonJSmain.js— following the contract below. Do not useimport/exportsyntax in the finalmain.js; everything the app offers arrives through thectxparameter.
What a plugin is
A folder with two files, dropped into your library:
<your Psi folder>/.rho/plugins/my-plugin/
├── manifest.json
└── main.js
No toolchain, no build step, no app rebuild. Edit the file, hit Reload, see it live.
Quick start (three minutes)
- Install Hello Panel from Settings → Plugins → Manage → Community plugins — or copy the starter template into the folder above.
- Open Settings → Plugins → Manage → Reload local plugins. Your plugin appears with a Local tag.
- Edit
main.js, hit Reload again. That's the whole development loop.
If loading fails, the Manage page shows an error card with a Copy error button — paste that into your AI together with your main.js, and it can usually fix the plugin in one round.
manifest.json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"description": "One line shown in the plugin list.",
"badge": "Beta",
"minAppVersion": "1.4.8"
}
Rules: id, name, version are required strings. id must equal the folder name and must not start with rho. (reserved for built-ins). badge and minAppVersion are optional. Optional entry overrides the default main.js.
main.js — the module contract
CommonJS, single file:
module.exports = {
activate(ctx) {
// register everything here, pushing disposables into ctx.subscriptions
},
deactivate() {}, // optional; the app also disposes ctx.subscriptions for you
};
Writing TypeScript or multiple files? Bundle to one CJS file:
npx esbuild src/main.ts --bundle --format=cjs --outfile=main.js
(An ES export default {...} bundle is also accepted.)
The ctx surface — what a plugin can do
Every capability arrives through the ctx parameter of activate. Push every registration's return value into ctx.subscriptions so disabling your plugin cleans up completely.
Commands
ctx.subscriptions.push(
ctx.commands.register({
id: 'my-plugin.doThing', // namespaced by your plugin id
title: 'My Plugin: Do Thing', // shown in the Command Palette (Ctrl+Shift+P)
run(arg) { /* ... */ },
}),
);
await ctx.commands.execute('rho.core.openFile', { path }); // call any command, incl. the app's
Your own icon in the Activity Bar (the convention)
Register a view — your plugin gets its own icon in the far-left column with its own sidebar stack. Reserve Explorer sections for ambient, glance-along content.
ctx.subscriptions.push(
ctx.views.registerView({
id: 'my-plugin.view',
title: 'My Plugin',
icon: '<svg ...>...</svg>', // inline SVG string
order: 90,
}),
);
Sidebar sections
ctx.subscriptions.push(
ctx.sidebar.registerSection({
id: 'my-plugin.section',
title: 'My Plugin',
view: 'my-plugin.view', // stack under YOUR view; omit → Explorer stack
order: 10,
mount(container, host) {
const el = document.createElement('div');
el.textContent = 'Hello';
el.style.color = host.theme.textColor; // match the app theme
container.appendChild(el);
return () => el.remove(); // cleanup on unmount
},
}),
);
mount gets a plain DOM container — no framework required. host.theme has mode, textColor, bgColor, borderColor, linkColor, hoverBg, uiFontSize, and cssVar(name); host.onThemeChange(cb) fires on theme switches.
Files (the workspace)
const psi = ctx.workspace.notesFolder(); // your Psi folder, or null
const text = await ctx.workspace.readFile(path); // rejects if missing
const made = await ctx.workspace.createFile(path, s); // create-if-absent (parents made)
await ctx.workspace.writeFile(path, s); // OVERWRITE (parents made)
const items = await ctx.workspace.listFolder(dir); // one level; [] if absent
const tree = await ctx.workspace.listMarkdownTree(dir); // every .md, recursive
await ctx.workspace.openFile(path); // open a tab
const dir2 = await ctx.workspace.pickFolder('Choose'); // native picker, null if cancelled
const p = ctx.workspace.joinPath(a, b, c);
If your plugin regenerates documents, gate your own overwrites: stamp files you generate (e.g. a generator: frontmatter key) and never overwrite a file that lacks your stamp.
Settings (persisted, namespaced to your plugin)
const n = await ctx.settings.get('count', 0);
await ctx.settings.set('count', n + 1);
ctx.subscriptions.push(ctx.settings.registerPanel({
id: 'my-plugin.settings',
title: 'My Plugin',
mount(container, host) { /* same contract as a section */ },
}));
Notifications & dialogs
await ctx.notifications.notify({ title: 'Done', body: 'Optional detail.' }); // OS notification
Prefer visible in-panel feedback for click responses — an OS notification alone is easy to miss.
Ship it
Add one entry to community-plugins.json via PR, pointing at your repo's raw manifest.json and main.js. Review is a quick safety look, not a code-quality gate — imperfect plugins are welcome. Users install and update yours from Settings → Plugins → Community plugins.
Trust model, stated plainly
A markdown document can never execute code in Rho MD. A plugin is different: it is software you explicitly install, and it runs with the same reach as the app. Install plugins you trust; share plugins worth trusting.