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 sayUseExample
"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).