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:

  1. Study it yourself — everything below is the real contract the app honors.
  2. 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.json and a CommonJS main.js — following the contract below. Do not use import/export syntax in the final main.js; everything the app offers arrives through the ctx parameter.

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)

  1. Install Hello Panel from Settings → Plugins → Manage → Community plugins — or copy the starter template into the folder above.
  2. Open Settings → Plugins → ManageReload local plugins. Your plugin appears with a Local tag.
  3. 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.

Open in Rho MD →