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: every host/* 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 and agent/event notifications) 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 as effect/Effect, convergence/effect, convergence/services; the effect barrel 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 (no enum, namespace with code, parameter properties or decorators). Own modules import with their extension (./grouping.ts); another plugin's files are reached only with import type, never at runtime. plugins/tsconfig.json checks the types; plugins/node_modules (pnpm install) is for tsc and node --test only, the app never reads it.
  • The official plugins are written in TypeScript with Effect and Zod, per plans/typed-plugins/STYLE.md (the plan plans/typed-plugins/PLAN.md explains why). Check and run them with pnpm -C plugins typecheck, pnpm -C plugins test (every *.test.ts and *.test.mjs under plugins/, through the Node resolver in plugins/testing/) and pnpm -C plugins format (Prettier). Tests may import convergence/effect/testing, whose testLayer records 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 function activate returns (docs/ui-plugins.md, Cleanup).
  • A program the plugin needs runs through api.process.spawn, asked for in the manifest (process for a named program such as gh, process.any for 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"]
  }
}
  • id is publisher/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 as local:<folder>. version is semver when there is an id. pluginApi is 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).
  • permissions lists everything the plugin does beyond drawing in its own views, each with a one-line reason the user reads (see Permissions).
  • main is the entry module. A manifest with ui fails with "use main instead of ui"; one with a main array (a native program) fails with "native plugins are not supported; use api.process.spawn", and so does one with legacyNative, for official plugins too (docs/process-plugins.md).
  • provides, inject and optional declare the typed services between plugins (docs/ui-plugins.md, Services): provides maps each service name (publisher/name) to the contract module inside the plugin that defines it; inject maps a needed service to the semver range the plugin needs (it waits, Waiting, until a provider runs one that matches); optional is the same for services the plugin uses when they are there. A name is never in two of the three.
  • contributes: hooks lists 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); rules are Markdown files whose text every agent session follows; themes are theme files; commands declares every command api.command registers (<name>.<command>, its title, category, default key and when); settings declares 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 with api.settings.get, never in the app's settings; a token or a password is a secret, 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-tools is a complete one. A tool can show its work as a subagent under its call (ctx.subagent), and a plugin with models.use can 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 as optional/subagents (convergence/subagents: the spawn_subagent tool 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.process and api.env call, every api.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:

  1. Read the release remotely with market_read and scan it with the checklist below. Write your result first: a one-line verdict, then the findings with files and lines.
  2. If it is safe, install it with market_install, then call market_request_enable with the folder it answered. The enable card shows the plugin, its permissions and your scan; the user decides.
  3. If you find a problem, do not install it. Explain the problem with files and lines, and offer market_report only 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:

  1. 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.
  2. Find the network endpoints and the spawned commands: every api.net.fetch, api.net.websocket and api.net.sse URL, and every api.process.spawn program and its arguments. Say where each one leads.
  3. 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.
  4. List bundled binaries: files that are not text, and the manifest's binaries. A binary cannot be scanned; say so.
  5. 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.
  6. Check market_similar for impersonation: a publisher or plugin name close to a well-known one (convergence is the only official publisher), a recent creation date and few installs are warning signs.
  7. Never run third-party code outside the plugin system: no node, bun, sh or npx on 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_report only 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 the attachments the 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:

  1. Find the cause in the related plugins' folders, and change as little as you can. Do not change version, id or basedOn. Add a permission only when the fix needs it, and say why.
  2. The plugins reload when you save: ask the user to try the fix.
  3. 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_publish for a fix.
  4. 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 with api.app.select; renames, checks off and archives them (chats.update).
  • chat (center, route chat): one pane view per chat, kept for the last three shown, with the transcript (rows built here from ui.markdown, ui.list, ui.diff, ui.code, buttons; placed in ui.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 a security_card node (ui.securityCard) and never answers them.
  • files (rail button + right panel): ui.tree from fs.search; a click shares openTab so the tabs plugin opens the file.
  • tabs (detail pane beside the chat): tab strip, ui.editor for files, ui.diff for diffs, markdown preview. Any plugin opens a tab with api.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 through git.watch and git_changed.
  • market (rail button + center route market): 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 link convergence://market/<publisher>/<name>[@version] opens a listing there.
  • settings (center route settings): appearance, agents, plugins, tools, Keyboard Shortcuts, updates, and one section for each plugin with settings (its fields from plugins.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 opens keybindings.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 with api.process.spawn and shares the provider SDK in sdk/ (docs/agent-protocol.md, Providers in JavaScript); each folder's NOTES.md records 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.