Plugins
Every pane kind but the terminal, the agent and the browser is a plugin: a package that Ovvi installs, loads in a web view and connects to the card's machine
A pane kind is what a pane shows: a terminal, an agent, a browser or a plugin. Terminal and Agent are herdr's own panes. Every other kind is a plugin: a herdr pane that runs a placeholder, with a label that names the plugin (Labels and instance ids). The app draws the plugin's card over that pane.
Every plugin is made the same way, as a pi package is:
- A plugin is a package: a folder with a
package.jsonwhoseovvikey is the manifest, its TypeScript and its ownnode_modules(The manifest). - The package builds itself with its own
buildscript, for example esbuild fromsrc/main.tsintodist/main.js. Ovvi ships no bundler. It runs the script with the npm, pnpm or bun on the user's loginPATH(Build a plugin). - Ovvi installs packages from npm, from git or from a local path. Settings ▸ Plugins lists them, and each plugin has its own page there: its settings, Show Log, Reload and Remove (Packages and installs).
- A plugin can declare settings in its manifest. The app draws them on the plugin's page and gives their values to its cards (Settings).
- Each card is a web view of its own. Its page calls the app through one
typed object,
ovvi(The plugin API). - The two default plugins, File Explorer and Git, are packages too. The app installs them on first launch, and they use only the public API (File Explorer and Git).
- The browser is a native web view. It is a kind in the same registry, with a manifest written in Swift (The browser).
| Pane kind | Content | Its code | How herdr marks the pane |
|---|---|---|---|
| Terminal | native terminal (libghostty) | the app | none (the default) |
| Agent | native terminal and chat view | the app | none (herdr sets pane.agent) |
| Browser | native web view of any site | the app | label ovvi:browser#<iid> <url> |
| File Explorer, Git | a plugin page in a web view | packages that the app installs in ~/.config/ovvi/plugins/ | label ovvi:<id>#<iid> <arg> |
| your plugins | a plugin page in a web view | packages on This Mac | label ovvi:<id>#<iid> <arg> |
How the pieces connect
~/.config/ovvi/plugins/csv-table/ package.json (the ovvi key), src/main.ts, node_modules/
│ npm run build: the package's own script, at install and on Reload Plugin
▼
~/.config/ovvi/plugins/csv-table/dist/ main.js, main.css, chunks
│ served as ovvi-plugin://csv-table/
▼
WKWebView, one per card ── the app's runtime makes `ovvi` and imports dist/main.js
│ ovvi.files.read(path) as a bridge message
▼
the bridge (Swift) ── bound to its card's machine and session
│ Exec of the card's machine
▼
This Mac, or a remote machine over the relay- The registry (
Registry) reads theovvikey of every package (Where the registry looks). A bad manifest is left out, and the plugin's log says why. - The package's build script writes the entry that the manifest names
(
dist/main.js), with its chunks and CSS beside it. The app runs the script at install when there is no built entry, on Reload Plugin, and on the card's Build. - The scheme handler serves the entry's folder as
ovvi-plugin://<id>/, under a strict Content Security Policy (Security and trust). The card loadsovvi-plugin://<id>/main, a page that the app writes: it links the app'scard.css, the entry's CSS when the build wrote one (dist/main.css), and the app's runtime,ovvi.js, with a module script for the entry so that both load at once. - The runtime reads the card's context, which the app puts in the page
before any script runs. It makes the
ovviobject, imports the entry and calls its default export with the object. - Each call of the page is a bridge message. Swift runs it with the
Execof the card's machine.
Where things live
| What | Where |
|---|---|
| Installed plugins (from npm or git, the defaults, New Plugin…) | ~/.config/ovvi/plugins/<folder>/ |
| Plugins from a local path | where they are, listed in ~/.config/ovvi/settings.json under packages |
| The list of packages | ~/.config/ovvi/settings.json, packages |
| A plugin's build | its own folder, for example dist/ |
| The API's types | ~/.config/ovvi/plugins/node_modules/@ovvi/plugin-api/, written at launch when the app's copy differs |
| The confirmed plugins' ids | ~/.config/ovvi/trust.json |
| The defaults' install records | ~/.config/ovvi/plugins/.defaults.json |
| Build output, console lines, errors | ~/Library/Logs/Ovvi/plugins/<id>.log (Errors and logs) |
| A card's state | in the app while it runs, and in the instance state file on the card's machine (State) |
| What the app carries | Ovvi.app/Contents/Resources/web/: the runtime (ovvi.js), card.css, the types package (plugin-api/), the defaults' source (defaults/<id>/) and the template (template/) |
- The folder is
~/.config/ovvi/and not~/Library/Application Support/, because people and agents type the path. It has no space, and agents know~/.config. - The launch argument
-configFolder <path>puts the whole folder somewhere else, and the plugin logs in itslogs/plugins. The UI tests use it, so they never touch the user's plugins or logs. - In the repository, the defaults are
plugins/builtin/explorerandplugins/builtin/git, and the template isplugins/builtin/template. The runtime and the types are inweb/ovvi/.make webinstalls, checks and builds the three packages as a user would, then copies their source withoutnode_modulesanddistinto.build/web, which the project copies into the app. plugins/extra/holds plugins that the repository keeps but the app does not carry (Tart VM).make lintchecks them; a user installs one from its folder (Packages).
Remote machines
A plugin lives and builds on the Mac. Its page runs in a web view on the Mac. Only its data comes from the card's machine:
ovvi.files,ovvi.gitandovvi.rungo through the card'sExec. On a remote machine that is the relay's one SSH session (Running commands). The page cannot pick another machine.- Paths in the API are the machine's paths. A watch on a remote machine is a check every 2 s while the card shows (Remote machines).
- Nothing is installed on the machine. One plugin on the Mac works for cards on every machine.
- A plugin folder on a remote machine is not loaded.
Memory
Each card has its own WebContent process, about 28 to 45 MB. Out of view, the 25 most recently shown web cards stay, browser and plugin cards together. An older one is dropped and made again from its label and its state when it shows (What stays in memory). A card with unsaved edits and a floated card are never dropped.
A missing plugin
A label can name a plugin that is not on this Mac: one that was removed, or one from another Mac. Its card shows "‹id› is not on this Mac" and the argument, with Show Placeholder (the pane's terminal) and Close Pane. When a package with that id exists but its manifest is wrong, the card says "‹id› did not load" and the field that is wrong, with the same two buttons (Show Log is in its menu). The app never changes the label, so the card comes back when the plugin does.
Not built
- The parts of the API in
Not in version 3, such as network
from the page beyond its manifest's local
connectservers. A plugin can usecurlon the machine throughrun. - Plugins from a project folder (
<repo>/.ovvi/plugins/), also on a remote machine. - Plugin settings from an agent: the
ovvicommand neither reads nor changes them. An agent on This Mac can editsettings.json, which applies at Reload Plugins. - Update and Remove from an agent: they stay the user's, on the plugin's page
in Settings. An agent on This Mac lists, makes, reloads and installs plugins with
the
ovvicommand (CLI). - A plugin store, and plugins outside panes (a sidebar section, a status item, a pill).
- Plugins on iPhone and iPad (iPhone and iPad).
Choices
The decisions are D89 to D94, D96 to D100, D102, D103, D107 and D108 on the
Decisions page. Research:
plugins.md,
plugin-systems.md.