Skip to content

Authoring a weaver

This is a guide, not the contract. What the platform guarantees is specified under openspec/specs/. For this page: surfaces · plugin-runtime · commands · menus · content-tabs · routing · surface-retention · containers · ui-primitives · theming · i18n. Where this page and a specification disagree, the specification is right, and that is a defect in this page: change the behaviour there, then explain it here.

A weaver is a LoomWeaver plugin, and all your domain UI and logic live in it. It imports only @loomweaver/plugin-sdk (nothing else is public API) and contributes through the uniform ctx it receives on activation. This page gives the shape of a weaver and maps the how-to pages that follow. ctx is the supported surface throughout. There is one deliberate way past it: defining your own custom element. That path is an escape hatch, and its costs are ones the platform cannot absorb for you.

Nothing in that contract changes how you write Angular. Your views are ordinary standalone components, the router is the one you already use, and the iframe sandbox is for code you did not write. The smallest useful weaver is a single surface that owns your whole route tree, which is how an application you already have moves in behind one plugin.

Where the snippets go. A snippet that starts with ctx. belongs inside activate(ctx) in your plugin file, src/lib/plugin/<id>.plugin.ts in a scaffolded weaver. Anything that belongs somewhere else names its file on the first line. Components live beside it under src/lib/views/, and providers always mean the providers array in the distribution’s src/app/app.config.ts. Samples has the same material as whole files you can copy in one piece.

The shape of a weaver

src/lib/plugin/notes.plugin.ts
import { Plugin } from '@loomweaver/plugin-sdk';
export const notesWeaver: Plugin = {
// Declares identity + the capabilities it needs; the distribution grants them (default-deny).
manifest: { id: 'notes', name: 'Notes', capabilities: ['contributions', 'ui', 'host'] },
// Called once when the plugin activates. Contribute through `ctx` here.
activate(ctx) {
// ctx.registerSurface / registerCommand / registerBarItem / registerRailItem / registerSettingsSection
// ctx.ui.* (dialogs, toasts, settings)
// ctx.host.* (version, update)
},
// Optional: clean up on deactivation (the host also disposes what you registered).
deactivate() {},
};

Every ctx.register* call returns a Disposable. Keep it if you want to remove a contribution yourself; otherwise the host disposes it when the plugin unloads.

Surfaces (the one contract): ctx.registerSurface is the author contract for anything the host renders. A Surface declares what it can do rather than where it lives: routable (URL-addressable), instanceable (multiple saved instances), docks (which regions may host it). The user arranges it from there.

Heavy surface? Defer it. Instead of component, give a loadComponent: () => import('./graph-view').then(m => m.GraphView). The host calls it the first time the surface is actually shown, and the pane renders the surface once it resolves. A surface that drags a chart or graph engine behind it therefore lands in its own chunk. A user who never opens it pays nothing for it. Everything else about the surface is unchanged.

Every surface needs an id and a title: the id is the surface’s stable handle (pick <plugin>.<surface>), the title is its tab label (and the fallback title when a deep-link auto-opens a tab).

Capabilities: the manifest declares what the plugin needs; the distribution grants it (provideCapabilityGrants). A declaration alone grants nothing. Using an ungranted surface throws CapabilityError. The coarse capabilities map to slices of ctx: contributions (register*), ui (ctx.ui.*), host (ctx.host.*), navigation (navigateContent/openContentTab/…), session (ctx.session), theme (ctx.contributeTheme), automation (ctx.invokeCommand/ctx.invocableCommands, for running actions other plugins contributed; your own need no grant). The user can also revoke any granted capability at runtime from the built-in Permissions settings. A revoked surface then throws CapabilityError on the next call. So treat a CapabilityError as a normal denial: catch it, rather than treating it as an invariant.

The pages

The order follows a weaver from its first surface to what it needs once it is shipped.

  • Surfaces in a sidebar: a docked surface, what its body may use, and the custom-element escape hatch.
  • A navigation tree in the sidebar: the tree the workbench draws from your declaration, marking where the user is, folding, and retitling the panel.
  • The content area: routable surfaces, tabs per pane, chromeless, closable, preview and pinned tabs, a surface’s own actions in the pane header, panes and tab groups.
  • Containers: a workspace in a tab, a child per item, relative addresses.
  • Sub-routes and follows: subRoutes, sub-tabs when the host mounts you off-router, the rest of the address, tabs that follow the selection, activeContent.
  • Commands and their triggers: one behaviour behind shortcut, bar and rail items, palette and menu.
  • Menus: registerMenuItem, a menu on a rail item, a bar item or a surface’s action, a slot that other plugins fill, ctx.ui.openMenu on your own view body, and the menu you draw in a sandbox.
  • Host UI and host facts: ctx.ui dialogs, toasts and progress, ctx.host.
  • View state that survives: VIEW_STATE: filter, sub-tab, scroll position in one shape.
  • Unsaved changes: DirtySurface and the Save, Discard, Cancel question.
  • Your plugin’s own store: ctx.state, one store shared by every surface of your plugin and kept per person.
  • Settings sections: registerSettingsSection, controls the host draws and you store.
  • Access gating: access on contributions and ctx.session.
  • Sandboxed surfaces: component or iframe, the sandbox bootstrap, the frame UI kit, the plugin store.
  • Translations: titles and labels as keys in a namespace of your own; body text stays yours.
  • Icons and theme: ctx.contributeIcons, ctx.contributeTheme.

Each page opens with what it does and when you need it, and closes with where to go next. Samples has the same material as whole files, and Concepts explains why the workbench behaves as it does.