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

TypeHigh-level APISurfaces that read it
commandK.commands.register(spec)Palette, menus, shortcuts, Actions panel, scripts
panelK.shell.registerPanel(spec)Docks, View menu, panel picker
settingK.settings.declare(spec)Preferences
widgetK.dashboard.registerWidget(spec)Dashboard
editorK.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

FieldDescription
idRequired. Registering without an identifier throws.
sourcecore by default. Otherwise the plugin identifier, or user.
titleA {fr, en} object, or a key from translations.js if the source is the core.
declarativeFlag. Contribution coming from a manifest, therefore known before any code runs.
Two title forms are accepted. A {fr, en} object when the contributor carries its own strings, which is the case for plugins, since they cannot write into the application dictionary. A translation key when the contributor is the core and the string lives in translations.js.

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.

// Addressing convention core.journal.toggle kernel panel or command app.outils.isofImport domain command (modal) app.donnees.importCSV domain command (modal) workflow.correction-blanc-v3 saved workflow, registered as a command phreeqc.simulation.lancer plugin, prefixed with its identifier user.* script or macro from the local zone

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.

Manifest Command visible, no handler Invocation Source activated Handler installed, then called

If activation completes without installing a handler, the dispatch fails explicitly: Commande sans gestionnaire après activation.

Tokens and disposal

var token = K.commands.register({ /* ... */ }); token.dispose(); // removes this contribution K.releaseSource('phreeqc'); // removes EVERYTHING the source registered

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.

An undisposed token leaves a ghost contribution after a plugin reload: the command stays in the palette, its code no longer exists, and the click fails silently. The workbench listens to registry.changed and destroys the matching panel as soon as the entry leaves the registry.

Events

EventPayloadEmitted 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.changedNoneWidget 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.

K.settings.declare({ id: 'phreeqc.timeout', source: 'phreeqc', title: { fr: 'Délai maximal', en: 'Timeout' }, default: 30 }); K.settings.get('phreeqc.timeout'); // highest-priority layer holding it K.settings.set('phreeqc.timeout', 60); // 'user' layer by default, persisted K.settings.onChange('phreeqc.timeout', fn);

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.

Inspecting

K.list('command').length; K.list('panel', function (p) { return p.source !== 'core'; }); K.commands.isAvailable('core.journal.toggle'); Array.from(K.context.all().entries()); // Developer mode only await K.commands.execute('core.dev.dump');
Being in the registry and being executable are two different things. Zone, capability and the when predicate are evaluated at dispatch, not at registration. K.commands.isAvailable(id) applies exactly the same rules as the palette.