Linking panels
How Plugin panels work together through the shared selection, the opener with witwin:// URIs, and events.
Plugins work together without importing each other. Three services carry almost every link between panels. Pick by what you want to say:
| You want to say | Use | Example |
|---|---|---|
| "this is the current thing" | selection (proposed) | the Hierarchy selects an object, Properties shows it |
| "show this thing" | opener (stable) | a Result row opens its view; a chat link opens a file |
| "this happened" | events (stable) | run.finished, project.file.created |
| "add this inside your UI" | a slot entry (Web API) | a Console channel |
Never call into another panel's store or component. If none of the four fits, the owner Plugin should offer a slot or a service.
Selection: the one current thing#
The window has one current Subject. Setting one of another kind replaces
the previous one (selecting a file clears the scene selection). selection
is a proposed service, so a third-party Plugin uses it only from a local
path with "selection" in enabledApiProposals.
const selection = host.services.get('selection');
selection.set({ kind: 'projectFile', path: 'data/field.npz' });
selection.subscribe((subject) => { /* show it, or ignore other kinds */ });Kinds: sceneObject (sceneId, objectIds), projectFile (path,
paths), result (resultId), study (studyId). A Plugin's own kinds are
<pluginId>.<name>; register the kind with selection.registerKind({ kind, clear }) so the window can clear it when another kind takes over.
The scene service also returns the scene selection (getSelection). That
is the same state, kept for Plugins that only care about scene objects.
Opener: show a thing by its URI#
opener.open(uri) asks each registered handler in order until one takes the
URI, and resolves true when one did. The caller does not know which panel
shows the thing; any Plugin can add a better viewer later.
import { fileUri } from '@witwin/workbench/services'; // system Plugins; third-party: build the string
const opener = host.services.get('opener');
await opener.open(fileUri('data/field.npz'), { preview: true });
await opener.open('witwin://scene/world/object/box#Material.color', { intent: 'inspect', reveal: true });URI form: witwin://<kind>/<id>[?key=value][#fragment]. Built-in kinds are
file, scene, result, study, run, parameter, panel; a Plugin's
own kinds are <pluginId>.<kind>. A kind is lowercase words joined by dots:
any number of owner segments (letters, digits, _, -), then a last word
(letters, digits, _). So witwin.example_notes.note is a valid kind of
the Plugin witwin.example_notes (KIND_PATTERN in
studio/packages/witwin-workbench/src/services/witwinUri.ts). The helpers
that build and parse URIs are listed in
Slots, events and URIs.
Options: intent (open the editor, or inspect to select and show in
Properties), preview (a tab the next preview replaces), reveal (frame a
scene object), title, secondary (the full viewer, used by a chat
double-click).
To show your own kind, register a handler. This one opens a tab of a panel
registered with multiInstance: true (openInstance needs it) through the
proposed panels service:
host.services.get('opener').registerHandler({
id: 'witwin.example_notes.note',
canOpen: (uri) => uri.kind === 'witwin.example_notes.note',
open: (uri) => host.services.get('panels').openInstance('witwin.example_notes.panel', uri.id),
});Handlers run in order (lower first). A third-party Plugin's handler is
asked only for URIs of its own kinds (<pluginId>.<kind>), so it cannot
take files or scene objects away from the panels that show them. Only a
system Plugin can handle a built-in kind; such a handler (file for
.npz) checks the details (the extension) in canOpen and leaves
everything else to the next handler. host.registerFileType adds an icon,
a name and a double-click command for an extension, not an opener handler
(Web API).
The Agent's chat links use the same URIs. host.registerChatLink adds the
hover card, the inline embed and the "does it still exist" check for your
kinds; clicking still goes through opener.
Events: something happened#
const events = host.services.get('events');
events.on('run.finished', ({ runId, status }) => refresh(runId, status));
events.emit('witwin.example_notes.reloaded', { count: 3 });Events are notices, not requests: nobody answers, and a listener must not
assume it is the only one. The typed events are in
Slots, events and URIs; a Plugin's own
events are <pluginId>.<what>. A third-party Plugin may listen to any event
but emit only its own names; any other name throws. Python halves have their own
ctx.events for server-side notices; crossing between the halves goes
through an RPC topic (ctx.rpc.topic, rpc.subscribe).