Convergence plugins
This folder is the plugin system of Convergence. Every part of the app that a user sees is a plugin in this folder: the sidebar, the chat view, the file tree, the git panel, the settings page and the agent providers. Convergence opens this folder as the "Plugins" workspace so a coding agent can change any of them in place. Saving a file reloads the plugin without restarting the app and without stopping running agents.
Read this file first, then docs/ for the full reference:
docs/ui-plugins.md: the plugin API (activate(api), slots, elements, kernel primitives, host API, events, commands, slash commands, files, network, programs, environment, timers, tools, hooks, agents, tool renderers, limits, hot reload).docs/host-api.md: everyhost/*method, its params, its result and the permission it needs.docs/events.md: every host event and the permission it needs.docs/tools.md: tools and rules a plugin gives every agent (code mode,api.tools.register).docs/agent-protocol.md: how an agent provider works (agent/*methods andagent/eventnotifications) and the event shapes.docs/process-plugins.md: running a program from a plugin (api.process.spawn), why no plugin is a native program, and the wire protocol the runtime speaks for a plugin.examples/: small complete plugins for each extension point.
Installing, updating, scanning or publishing a plugin from the marketplace: read Marketplace below first.
One runtime
Every plugin is JavaScript or TypeScript. It runs in the plugin host, a helper process
the app starts under the OS sandbox, in a QuickJS runtime of its own: the
official plugins share one helper, every other plugin gets its own. The
helper can open no file, socket or program; a plugin reaches files, the
network, programs and the environment only through the host (api.fs,
api.net, api.process, api.env), and only within its grants. A plugin
that uses too much memory or runs JavaScript for too long at once is
stopped, not the app (docs/ui-plugins.md, Limits).
What that means for code:
- No Node or browser built-ins beyond the ones the runtime gives (timers
TextEncoder,TextDecoder,atob,btoa,AbortController,Headers,console) and no npm packages: import the plugin's own files with./paths, and the built-in modules (convergence,zod,effect/<Module>such aseffect/Effect,convergence/effect,convergence/services; theeffectbarrel is refused, it would load every module). A library is copied into the plugin folder. - A module may be TypeScript (
.ts,.mts): the runtime erases its types, so only syntax that erases runs (noenum,namespacewith code, parameter properties or decorators). Own modules import with their extension (./grouping.ts); another plugin's files are reached only withimport type, never at runtime.plugins/tsconfig.jsonchecks the types;plugins/node_modules(pnpm install) is fortscandnode --testonly, the app never reads it. - The official plugins are written in TypeScript with Effect and Zod, per
plans/typed-plugins/STYLE.md(the planplans/typed-plugins/PLAN.mdexplains why). Check and run them withpnpm -C plugins typecheck,pnpm -C plugins test(every*.test.tsand*.test.mjsunderplugins/, through the Node resolver inplugins/testing/) andpnpm -C plugins format(Prettier). Tests may importconvergence/effect/testing, whosetestLayerrecords host calls with in-memory fakes. - What a plugin starts outside its runtime (a program, a host watch) it
stops in
api.onUnload(fn), or in the functionactivatereturns (docs/ui-plugins.md, Cleanup). - A program the plugin needs runs through
api.process.spawn, asked for in the manifest (processfor a named program such asgh,process.anyfor anything else). - Plugins share nothing but the app state (
api.app) and the host: one plugin's globals are not another's. - Everything is asynchronous: a host or kernel call returns a Promise.
Layout of a plugin
plugins/<name>/
plugin.json manifest (required)
main.js the entry ("main": "main.js")
... other modules, JSON files, anything else the plugin needs
plugin.json:
{
"id": "alice/hello",
"name": "hello",
"version": "0.1.0",
"description": "One line.",
"pluginApi": ">=2 <3",
"main": "main.js",
"permissions": {
"ui.slots": { "slots": ["right"], "reason": "Show the Hello panel" },
"fs.read": { "scopes": ["workspace"], "reason": "Count the files of the workspace" },
"net": { "hosts": ["api.github.com"], "reason": "Load pull requests" }
},
"contributes": {
"hooks": ["before_prompt", "after_turn", "tool_call"],
"themes": ["theme.json"],
"rules": ["rules.md"]
}
}
idispublisher/name(lower case letters, digits and-; the publisher at most 39 characters, the name at most 64). A plugin without one is a local plugin, known aslocal:<folder>.versionis semver when there is anid.pluginApiis the range of plugin API versions the plugin works with (this app is version 2).basedOn: { id, version }marks a changed copy of another plugin (a mod).permissionslists everything the plugin does beyond drawing in its own views, each with a one-linereasonthe user reads (see Permissions).mainis the entry module. A manifest withuifails with "use main instead of ui"; one with amainarray (a native program) fails with "native plugins are not supported; use api.process.spawn", and so does one withlegacyNative, for official plugins too (docs/process-plugins.md).provides,injectandoptionaldeclare the typed services between plugins (docs/ui-plugins.md, Services):providesmaps each service name (publisher/name) to the contract module inside the plugin that defines it;injectmaps a needed service to the semver range the plugin needs (it waits,Waiting, until a provider runs one that matches);optionalis the same for services the plugin uses when they are there. A name is never in two of the three.contributes:hookslists the hooks the host may call (api.hooks.on), as a name or{ "hook": "tool_approval", "order": -10 }(default 0; lower first, ties by plugin name);rulesare Markdown files whose text every agent session follows;themesare theme files;commandsdeclares every commandapi.commandregisters (<name>.<command>, its title, category, defaultkeyandwhen);settingsdeclares the plugin's settings, which the settings page draws as its own section (docs/ui-plugins.md, Commands and keys, Settings). Everything else (views, slash commands, tools, agents, tool renderers) is registered from code.- A setting of the plugin's own belongs in
contributes.settings, read withapi.settings.get, never in the app's settings; a token or a password is asecret, which only the plugin reads (api.settings.secret) and nobody writes into a file. A permission can name a setting as a whole scope entry ("hosts": ["${settings.serverUrl}"]): the user approves each new value on a card, and the plugin cannot change that setting itself. - Tools every agent can call come from
api.tools.register(docs/tools.md);examples/hello-toolsis a complete one. A tool can show its work as a subagent under its call (ctx.subagent), and a plugin withmodels.usecan run prompts through the user's agents and define subagent types (docs/ui-plugins.md, Models and subagents). optional/holds official plugins the marketplace offers but the app does not install, such asoptional/subagents(convergence/subagents: thespawn_subagenttool and the composer's agent/model/effort@tags). The app does not load them from there; install one from the marketplace.
Permissions
A plugin asks for permissions in its manifest; the user grants them on a
card the app draws itself, which no plugin can draw, cover or click. Until
then the plugin does not run. The host and the kernel check every call:
one without a grant fails with PermissionNotGranted and does nothing,
and the rest of the plugin keeps working.
| Permission | Scope field | What it allows | Risk |
|---|---|---|---|
fs.read, fs.write |
scopes: workspace, plugin (its own folder), data (its own data folder, api.paths.data), or a path glob (/abs/**, ~/x/*.json) |
fs.* host calls on those paths; a link counts by where it lands, so a link out of a granted folder needs a scope for its target |
medium; high for a glob over ~ or / |
net |
hosts: whole host names, localhost:<port>, localhost:* (no *.host) |
api.net to those hosts, and links api.openUrl opens without a click |
medium |
process |
programs: program names (not shells, interpreters or launchers such as sh, node, python, npx, env, nohup, busybox, awk, java, open; the full list is INTERPRETERS in crates/protocol/src/permissions.rs) |
api.process.spawn and api.process.which of those programs |
medium |
approvals.auto |
answer the agent's approval requests for you (tool_approval gate) |
dangerous | |
process.any |
running any program, by path or through an interpreter | dangerous | |
native.binaries |
shipping its own binaries in binaries (needs process.any) |
unsafe | |
env |
vars |
api.env.get of those variables |
medium |
chats.read |
workspaces, chats and transcripts, and their events | medium | |
chats.control |
creating, changing and deleting chats and workspaces; prompts; answers to questions | high | |
agents.provide |
providing agents (api.agents.register, and passing their tool calls to plugin tools); api.openUrl of a web link while the user signs in to one of its agents |
medium | |
agents.manage |
enabling agents, signing in and out, updating them | high | |
models.use |
running prompts through the user's enabled agents in hidden sessions (api.models), and defining and running subagent types (api.subagents); the user's own accounts pay |
high | |
tools.provide |
giving every agent tools (api.tools.register) |
medium | |
settings.read, settings.write |
the app settings | low / high | |
git.read, git.write |
git status and diffs / any git command | low / high | |
terminal |
terminals and their events | dangerous | |
ui.slots |
slots: left, center, detail, right, rail, bottom, status, title |
views in those shell slots | low |
commands.run |
running other plugins' commands and palette modes | high | |
market, plugins.manage |
the marketplace; managing plugins | official plugins only | |
storage, settings.own |
its own storage and settings | always granted |
"permissions": {
"net": { "hosts": ["api.github.com"], "reason": "Load pull requests" },
"process": { "programs": ["gh"], "reason": "Create pull requests with the GitHub CLI" }
}
An unknown permission, a scope field the permission does not have, a
permission that needs a scope with none listed, or a missing reason fails
the manifest: the plugin shows as failed with the message. So does a
setting template (${settings.key}) that is not a whole entry, names no
declared setting, or names one whose type cannot fill the permission
(url/host for net, path for fs.*, a select of program names or
a file path for process).
- A new plugin does not run until the user enables it in Settings >
Plugins ("Review and enable…"). The app shows a card with every
permission, its reason and its risk, and nothing is granted until the
user clicks Enable there (for
process.any, holds it for two seconds). - A plugin whose manifest asks for more later keeps what it has; the new permissions wait for "Review permissions…" in Settings > Plugins, and the calls that need them fail until then. The user can revoke any grant there.
- Plugins with a
convergence/id in the app's own folder (the built-in ones) are official: they get what they ask for at every load. A copy that someone changed in an installed app is no longer official; it "claims an official id" and waits for its card like any other (a shield in the title strip opens it). - Ask for what the code uses and nothing more, with a reason a user
understands in one line. Every
api.host.*,api.fs,api.net,api.processandapi.envcall, everyapi.host.kernel(...)call, shell slot and link needs its permission (docs/host-api.md,docs/ui-plugins.md).
When you make or change a plugin for the user, tell them what to do next: a new plugin needs Settings > Plugins > "Review and enable…", and a plugin that asks for new permissions needs "Review permissions…". Name the permissions you added and why. You cannot enable a plugin or grant it anything yourself, and neither can any plugin.
Marketplace
Plugins come from, and go to, the Convergence marketplace. In this
workspace you have the marketplace tools of the official market plugin:
use them first. cvg, the marketplace CLI, is second: it is on PATH in the
Plugins workspace's sessions, for what the tools do not cover. Reading and
downloading need nobody's approval, because a downloaded plugin is not
enabled and cannot run. Enabling, publishing, reporting, bug reports and
fixes are the user's: each is a card the app draws itself, which you only
ask for.
The tools
| Tool | What it does | What the user sees |
|---|---|---|
market_search { query?, category?, sort?, page? } |
searches the listings (id, publisher, official flag, latest version, installs, rating) | nothing |
market_read { id, version?, path? } |
a release read from its verified package, without installing it: the file tree, permissions, basedOn and publisher, or with path one file's text. Nothing is saved, nothing runs |
nothing |
market_similar { id } |
publishers and plugins with similar names, with creation dates, install counts and the verified flag | nothing |
market_install { id, version? } |
verifies, downloads and extracts a release into this folder, not enabled; answers the folder | nothing |
market_update { name } |
updates an installed plugin by folder: an unmodified official plugin is replaced at once; anything else answers needsAgent with the versions, the local changes and the permission changes, for you to merge |
nothing |
market_request_enable { name } |
asks for the enable card of a folder in this chat (the permission card when it is enabled and asks for more) | the enable card |
market_publish { name, visibility?, share? } |
only when the user asked you to publish: puts the publish row in this chat | the publish row |
market_report { id, version, reason, files } |
only when you are highly confident (Reports below): prepares a report | the report card, with exactly what is sent |
market_bug_report { plugins, preview: true } |
lists the attachments a bug report would send (Bug reports below); sends nothing | nothing |
market_bug_report { text, plugins, attachments?, fullSource?, contactEmail? } |
only when you are highly confident, after the user checked the text and chose the attachments: prepares a bug report | the send card, with the full preview of everything that is sent |
market_attach_fix { report, folders? } |
after a fix works: asks for the attach card of a bug report | the attach card: the diff, the changed permissions, the contributor agreement on a first contribution |
cvg
A plugin's settings (never its permissions, whether it is on, or a secret)
change from a terminal while the app runs: cvg settings get <folder> [key] and cvg settings set <folder> <key> <json> (plain text that is not
JSON is a string; null resets the default). The change applies at once,
as if the plugin had set it; a setting a permission refers to is the
user's alone, in Settings. The theme is the themes plugin's settings:
presets (your presets by id: { name, mode, tokens }, tokens as in
ThemeTokens in types/convergence.d.ts) and active ({ light, dark },
a preset id each, stock ids convergence-*, vscode-*,
high-contrast-*, compact-*). For example, a dark preset with larger
type, chosen for dark mode:
cvg settings set themes presets '{"big":{"name":"Big","mode":"dark","tokens":{"type":{"baseSize":17,"textSize":14}}}}'
then cvg settings set themes active '{"light":"convergence-light","dark":"big"}'.
Read presets first and write the whole object back: a set replaces it.
The same marketplace from a terminal, for scripts and CI: cvg search,
cvg info <id>, cvg read <id>[@version] [path], cvg similar <id>,
cvg install <id>[@version] [--chat <chatId>], cvg update [folder],
cvg list, cvg status <folder>, cvg diff <folder> (the local changes
against the release it came from), cvg request-enable <folder> [--chat <chatId>], cvg init <name>, and the account (cvg login, cvg whoami).
Add --json to read the output. cvg install lands not enabled, as the
tool does. In a chat prefer the tools: they put the cards in the right
chat. Never run cvg publish or cvg report for the user: publishing
goes through the publish row and reporting through market_report, so the
user sees what leaves. cvg bug [--plugin <folder>] [--text <text>] opens
the app's bug report form, and cvg bug attach <report> [--folder <f>]
the attach card of a fix; both need the app running, and only the user's
click on the card sends anything. cvg contrib apply <report> is for the
maintainers (it needs an admin account): it checks that each fix's base
version is the version in the repository and applies the fixes on a new
branch, without committing or pushing.
Installing a plugin
The user's Install button opens a chat here with the listing id, the version and the scan prompt. Then:
- Read the release remotely with
market_readand scan it with the checklist below. Write your result first: a one-line verdict, then the findings with files and lines. - If it is safe, install it with
market_install, then callmarket_request_enablewith the folder it answered. The enable card shows the plugin, its permissions and your scan; the user decides. - If you find a problem, do not install it. Explain the problem with files
and lines, and offer
market_reportonly as Reports below allows.
A plugin with process.any or native.binaries can run any program:
name every command and binary you found in your result, since the card
shows them in red and the user must hold Enable to turn it on.
Scanning a plugin
Scan every plugin before it is installed or updated, the official ones included:
- Compare the permissions with the code. Every permission must be used for the reason it states, and the code must not reach for more. A permission nothing uses is a finding too.
- Find the network endpoints and the spawned commands: every
api.net.fetch,api.net.websocketandapi.net.sseURL, and everyapi.process.spawnprogram and its arguments. Say where each one leads. - Find obfuscated, encoded or minified code: long base64 or hex
strings,
eval,new Function, code built from strings, packed or minified bundles you cannot read. Code you cannot read is not safe. - List bundled binaries: files that are not text, and the manifest's
binaries. A binary cannot be scanned; say so. - Diff a mod against its base: for a listing with
basedOn, read the base release (market_read { id: basedOn.id, version: basedOn.version }) and scan what the mod adds or changes. - Check
market_similarfor impersonation: a publisher or plugin name close to a well-known one (convergenceis the only official publisher), a recent creation date and few installs are warning signs. - Never run third-party code outside the plugin system: no
node,bun,shornpxon its files, no installing its dependencies, no opening its links. Read it only.
Mods
A mod is a changed copy of another plugin (basedOn: { id, version },
filled in by the app when it is published). If the original is not
installed here, install the mod as any plugin. If the original or another
mod of it is installed, read three trees remotely (the base at
basedOn.version, the mod, and the local copy here), merge the mod into
the local copy so the user's own changes stay, and ask for the enable card
only if the permissions changed.
Updating
An update chat starts with what market_update answered: the versions,
the local changes against the installed release and the permission
changes. Read the new version with market_read, scan the difference
with the checklist, merge it into the local copy (keep every local
change), and call market_request_enable only if the permissions
changed. An unmodified official plugin needs no chat: the app replaces it.
Publishing
After a turn that changed a plugin here, the app itself shows a publish
row below your response (for a plugin with no publish setting that is not
an official plugin as shipped), with the plugin and its permissions.
Do not ask the user whether to publish, and do not call market_publish
on your own. When the user asks you to publish, check the manifest first:
a semver version higher than the published one, pluginApi, and a
one-line reason for every permission. Then call market_publish; the
user publishes from the row. The app fills in basedOn and signs the
upload; a changed official plugin is published as the user's mod.
Reports
A report goes to the marketplace's moderators, and a false one is noise
that costs them time. Report a plugin with market_report only when you
are highly confident that it is malicious or has a security
vulnerability, and name the files and lines that prove it
(files: ["main.js:42", "lib/net.js:10-18"]). Never report on a hunch, for
style, for a missing feature or for an ordinary bug. Tell the user what you
found first; the report card then shows exactly what would be sent, and
nothing is sent unless the user sends it.
Bug reports and fixes
A bug report goes to the Convergence maintainers (and, for a third-party plugin, to its publisher), with attachments the user chooses: the diff of a changed official plugin against its base version, a third-party plugin's full source when chosen, config with secrets removed, diagnostics and screenshots. Maintainers read every report, so it must not be noise.
- Use
market_bug_reportonly when you are highly confident that the bug is real and you can name the plugin. Not for questions, wishes, or a problem the user can change in Settings. - Before you call it, show the user the report text you propose and the
attachments you propose (
market_bug_report { plugins, preview: true }lists them with their ids and sizes, and sends nothing). Ask the user to check the text and to choose what to attach. Only then call it, with the text and theattachmentsthe user chose. The native card shows the full preview of everything that is sent; nothing is sent unless the user sends it there. - A user who is not signed in may give a contact email; never invent one.
Most bugs can be fixed in the plugins here. A chat opened with "Try to fix it with an agent" starts with the report's number and text:
- Find the cause in the related plugins' folders, and change as little as
you can. Do not change
version,idorbasedOn. Add a permission only when the fix needs it, and say why. - The plugins reload when you save: ask the user to try the fix.
- When a turn changed an official or marketplace plugin, the app shows an
"Attach fix to report #N" row below your response, in place of the
publish row. The user reviews the diff, the changed permissions and,
on their first contribution to an official plugin, the contributor
agreement on its card, and attaches the fix there. Do not call
market_publishfor a fix. - When the row is gone (a later turn changed nothing), or the fix is for
another report, call
market_attach_fix { report }once the user confirmed the fix works.
A fix to a third-party plugin goes to its publisher as a suggestion.
The smallest plugin
import { html } from "convergence";
export function activate(api) {
api.slot("right", (ctx) => {
ctx.state.count ??= 0;
return html`
<div display="flex" flexDirection="column" padding="medium" gap="small">
<text style="heading">Hello</text>
<text style="body">Clicked ${ctx.state.count} times</text>
<button id="hi" label="Click" onClick=${() => { ctx.state.count++; ctx.update(); }} />
</div>`;
}, { title: "Hello" });
}
Its manifest asks for the one slot it draws in:
"permissions": { "ui.slots": { "slots": ["right"], "reason": "Show the Hello panel" } }.
html templates need no build step. div is the general container: its
style props, theme tokens, data-only events (onClick, onPointerMove,
onKeyDown, ...), canvas (a draw list) and motion (animations the
kernel runs) are in docs/ui-plugins.md, Templates and div. Native
components are tags of their own (<button>, <input>, <select>,
<popover>, <scroller>, <markdown>, ...). The older form still works:
ui.column and ui.row are shortcuts that make such a div, so this is
the same view:
return ui.column({ padding: "medium", gap: "small" }, [
ui.heading("Hello"),
ui.text(`Clicked ${ctx.state.count} times`),
ui.button({ id: "hi", label: "Click", onClick: () => { ctx.state.count++; ctx.update(); } }),
]);
Every built-in UI plugin is written with templates; git/main.js and
sidebar/main.js show the patterns (a few layout objects such as
column and row spread into each div). examples/chart draws with
canvas, follows the pointer and animates with motion.
examples/hello-settings declares settings of every type (with a custom
section view), a net permission that takes its host from a setting, a
secret, commands with a key and a when key, and a palette mode.
Slots: title (the strip above the main area), left (sidebar), center
(main pane), detail (tabbed pane beside the main pane), right (panel
before the rail; plugins show it when ctx.app.shared.rightPanel names
them), rail (one icon button per view; order >= 100 sits at the bottom),
bottom, status. A view returns null to hide itself (for example when
ctx.app.route is not its route). A view in any other slot name is not
placed by the shell; another view embeds it with ui.view({ view: id })
(the chat plugin's pane views, one per chat).
Rules of the built-in plugins
sidebar(left): workspaces and their chats (chats.list) in status sections, never grouped; selects chats withapi.app.select; renames, checks off and archives them (chats.update).chat(center, routechat): onepaneview per chat, kept for the last three shown, with the transcript (rows built here fromui.markdown,ui.list,ui.diff,ui.code, buttons; placed inui.scroller) and the prompt composer (input, selects, list, buttons). Streaming re-renders the pane of that chat only. Approvals, plugin requests, the publish row and report cards are the app's own cards: the plugin places them with asecurity_cardnode (ui.securityCard) and never answers them.files(rail button + right panel):ui.treefromfs.search; a click sharesopenTabso thetabsplugin opens the file.tabs(detail pane beside the chat): tab strip,ui.editorfor files,ui.difffor diffs, markdown preview. Any plugin opens a tab withapi.app.share("openTab", { kind: "file"|"diff", workspaceId, path, diff?, nonce: Date.now() }).terminal(rail icon + hover fly-out + detail tabs): the integrated terminal. Hovering the icon at the foot of the rail opens a wide, short window with the tab strip and the live shell; a terminal pops out into a tab of the detail pane for the full height. New, close, rename (from the tab's right-click menu), one set of shells per workspace. The kernel runs the shells and draws the grid; everything else is here.git(rail button + right panel): source control like VS Code's: commit box, Merge/Staged/Changes sections with hover actions, branch picker, sync, pull/push/fetch/stash menu; file diffs open as tabs. It follows the repository throughgit.watchandgit_changed.market(rail button + center routemarket): the marketplace: browse, search, listings with their files, permissions and comments, Install (a scan chat here, with the agent, model and effort next to the button), update notices (the rail badge and an Update button per plugin), sign-in, and the marketplace tools of this workspace (Marketplace above). A linkconvergence://market/<publisher>/<name>[@version]opens a listing there.settings(center routesettings): appearance, agents, plugins, tools, Keyboard Shortcuts, updates, and one section for each plugin with settings (its fields fromplugins.settings, its commands and keys, its permissions with Manage). The Plugins page lists every plugin with its badges, state and grants; enabling and new permissions go through the app's card (plugins.requestEnable), and so does a changed setting a permission refers to (plugins.set_setting). Keyboard Shortcuts searches every command, records a key natively (keys.record), shows conflicts with Assign, resets, and openskeybindings.json.palette(a view in the title strip that is only an overlay): ⌘⇧P lists every command with its key (a command in conflict with Assign, which opens Keyboard Shortcuts), ⌘P searches chats by title and workspace, and other plugins add modes (api.palette.provider). It runs what the user picks in the plugin that owns it (commands.run).codex,claude,opencode,acp: the agent providers. Each runs its agent's own program withapi.process.spawnand shares the provider SDK insdk/(docs/agent-protocol.md, Providers in JavaScript); each folder'sNOTES.mdrecords how it maps its agent.
Replace any of them by editing its folder, or by adding a new plugin that renders into the same slot, enabling it, and disabling the built-in one in Settings.
A change to a built-in UI plugin that must not change what it shows (a
refactor) is checked without a window: copy the plugin's folder to
<folder>/<name> before the change (for example under target/), then run
CONVERGENCE_UI_BASELINE=<folder> cargo test -p convergence-kernel --test plugin_equivalence. The test runs
the old and the new copy side by side on the same fixture data, fires every
handler their views show, and requires equal trees, app state and host
calls after every step (docs/ui-plugins.md, Checking a view).
Native components
No feature is native. The kernel keeps only generic stateful primitives,
keyed by id, whose state survives re-renders and reloads:
| Element | Purpose |
|---|---|
ui.markdown(text, { id }) |
rendered markdown; with an id, streaming appends are cheap |
ui.scroller({ id }, items) |
virtualized list that follows its end (transcripts, logs) |
ui.tree({ id, items, selected, onSelect }) |
collapsible tree with retained expansion |
ui.editor({ id, text, language, readOnly }) |
highlighted editor with line numbers |
| `ui.diff({ path, diff | oldText, newText, maxLines })` |
ui.terminal({ terminal }) |
the grid of a shell the kernel runs (terminal.create gives the id) |
ui.input, ui.select |
text and choice controls |
Reloading
The app watches this folder. A changed plugin loads again at once: its
runtime keeps running while the new code loads, its views keep their
identity (see docs/ui-plugins.md, Views), so inputs keep their text and
focus, and the old trees stay on screen until the new code has its data.
A changed provider restarts at once unless one of its agents has an active
run, in which case it restarts when that run finishes (Settings > Plugins
shows "Reload pending"). A plugin that fails to load, or that its limits
stopped, is shown failed in Settings > Plugins with its error; the rest of
the app keeps working.
What each kind of change needs before it shows:
| Changed | Needed |
|---|---|
a plugin (*.js, *.ts, *.json, plugin.json, a theme) |
nothing; it reloads on save (new permissions of a plugin that is not official wait for the user's review) |
the app itself (crates/: kernel, host, protocol, the plugin host in crates/sandbox, whose prelude is the convergence module) |
a new app: cargo run -p convergence for the dev app, or pnpm install:macos:dev for the installed Convergence Dev, which quits and reopens it. Never pnpm install:macos:alpha: Convergence Alpha is the stable app the user works in, and only the user promotes it. |
Both installed apps read this folder, so a changed plugin (a provider too) reloads in both at once, while each app keeps the host and plugin helper it was installed with. A plugin that needs a kernel or runtime feature Alpha does not have yet must fail softly there.
Nothing in this folder can change the native controls (the select, the
input, the tabs): those live in crates/kernel, and a plugin only describes
what it wants drawn. Say so instead of editing the kernel from a plugin
task.
Source: plugins/AGENTS.md in the Divergence repository, built with this site.