The contribution registry
One registry per type, behind a single registration call: K.contribute(type, entry). The type is not a closed list, so a new contribution point can be declared without touching the kernel. Every registration returns a disposal token, aggregated per source, which is what makes it possible to unload a plugin without the kernel knowing anything about the nature of its contributions.
Types in use
| Type | High-level API | Surfaces that read it |
|---|---|---|
| command | K.commands.register(spec) | Palette, menus, shortcuts, Actions panel, scripts |
| panel | K.shell.registerPanel(spec) | Docks, View menu, panel picker |
| setting | K.settings.declare(spec) | Preferences |
| widget | K.dashboard.registerWidget(spec) | Dashboard |
| editor | K.editors.register(spec) | Central-zone tabs |
Tasks and connectors do not go through contribute: the workflow engine keeps its own table (K.workflows.declarerTache), and so does the connector registry. Both still return a disposal token and emit an event on every mutation.
Contribution envelope
| Field | Description |
|---|---|
| id | Required. Registering without an identifier throws. |
| source | core by default. Otherwise the plugin identifier, or user. |
| title | A {fr, en} object, or a key from translations.js if the source is the core. |
| declarative | Flag. Contribution coming from a manifest, therefore known before any code runs. |
Replacement rule
A source may replace its own contribution. The case is not a duplicate: a plugin declares a command in its manifest, then, once activated, registers its real implementation under the same identifier.
A source may never overwrite another source's. The kernel logs a warning and returns an inert token. This rule stops a plugin from taking over the kernel's own navigation by declaring core.go.dashboard.
Declarative versus imperative
The declarative flag distinguishes what comes from a manifest, and is therefore known before any execution, from what comes from already-running code. A declarative command has no handler: it shows up in the palette, and invoking it wakes its source.
If activation completes without installing a handler, the dispatch fails explicitly: Commande sans gestionnaire après activation.
Tokens and disposal
Every token is added to a per-source aggregate. Disposal happens in reverse order of registration: a late contribution may depend on an earlier one, never the other way round. releaseSource() replaces manual teardown, which had to enumerate every sink and risked missing one.
Events
| Event | Payload | Emitted by |
|---|---|---|
| registry.changed | {type, id, added} | Kernel, on every mutation. |
| command.before | {id, args} | Kernel, after the guards, before the handler. |
| command.after | {id, args, result} | Kernel, on success. |
| command.error | {id, args, error} | Kernel, on refusal or failure. |
| setting.changed | {id, value, layer} | Settings. |
| workflow.tasks.changed | {id} | Workflow engine. |
| dashboard.changed | None | Widget registry. |
Cancellable hooks
K.events.emitBlocking(name, payload) waits for its subscribers and returns false if one of them cancelled. This is the clean injection point for override scripts, which can refuse a save or an engine run without touching the core. Subscribers may be asynchronous.
Layered settings
Four layers, from lowest to highest priority: default, edition, user, project. The effective value is computed on read, never frozen on write: otherwise an override installed late would have no effect on anything that had already read.
The native menu does not follow the registry
The menu bar is native, built in Rust at startup, and never rebuilt. The View menu lists the kernel panels, known at compile time, then an All panels... entry which reads the registry at the moment it is opened. That picker is the only place where plugin panels appear.
The Rust-to-JS bridge goes through window.IsoFindPanelMenu.activer(rank), which executes the matching <id>.toggle command. Check marks are resynchronised on registry.changed and command.after.