Views & surfaces
Plugins ship UI in three shapes, from least to most powerful: declarative view specs that render a directory as a table or board with no code, ESM code viewers that render run-tree rows and page types with a real React component, and surfaces that claim a whole special-page route. All three run inside the same host-pinned, CSP-locked sandbox, so a plugin’s UI has DOM access but no remote script and no cross-origin exfiltration channel, and any crash degrades gracefully instead of taking the app down. This page builds on views and connects to page types.
Declarative view specs
Section titled “Declarative view specs”The simplest plugin view is pure data. A view-spec element renders a dir as a table or
board over the pages in a knowledge-base directory (kb.dir.pages), with filters,
grouping, and column selection expressed as JSON. There is no code to review and nothing to
sandbox: the first-party renderer interprets the spec, so a declarative view carries no
new trust at install. This is the lane the app builder
and most CRM-style dashboards use.
{ "kind": "dir", "layout": "board", "dir": "books", "groupBy": "status", "columns": ["title", "author", "rating"] }ESM code viewers
Section titled “ESM code viewers”When a view needs real logic, a plugin ships a React component as plain ESM under views/
and declares it in viewers: [{ id, src, meta? }]. The host exposes only
/plugins/<name@version>/views/*.js to browsers (an immutable cache, since version
directories never change); the manifest and pages/ never ship to the client.
-
The async viewer registry resolves an id
When the SPA meets a run-tree row whose
viewerid it does not recognize, the async viewer registry resolves the id to a URL throughplugins.viewersand dynamically imports the bundle. -
Host libraries are pinned by an import map
react,react/jsx-runtime,react-dom, and@subspace/uiresolve to the host’s copies through an import map injected into the page, so a viewer shares the app’s React instance instead of bundling its own. In dev the map points at host source; a build emits stable/plugin-host/*.jsentries. -
The component receives a row
A viewer is called with
{ row: { kind, tool?, title, args?, output? } }and returns UI.
Declarative viewer meta
Section titled “Declarative viewer meta”A viewer entry’s meta is data only, not code:
{ step, tool?, title (a template like "search → {{args.q}}"), redact (JSON paths) }. The
first-party interpreter compiles it and appends it to the run tree’s row-description
resolution. Crucially, templates run on the redacted input, so a title cannot leak what
redact removed (for example args.secret renders as [redacted]), and a plugin’s meta
entry can never shadow a first-party binding.
Surfaces
Section titled “Surfaces”A surface is a plugin-owned special page. Declaring
surfaces: [{ slug, src, title? }] claims the route /p/:slug, rendered by a plugin ESM
component from views/*.js. Installing registers each surface as an element page (kind
element, elementType plugin-surface, root-level, provenance-tagged with
metadata.surface {src, title?}), so the server treats it like a core special page: it is
routable at /p/:slug, listed in the sidebar’s special section, and carries
page ACLs exactly like /inbox or /agents.
The SPA resolves the component through plugins.surfaces (enabled plugins only) and imports
it via the same import-map machinery as viewers; the component receives
{ page: { id, title, slug } }.
Page-type default views
Section titled “Page-type default views”The three shapes compose through page types. A page type
can name a defaultView, and pages in that type’s namespace (its owning directory slug)
auto-render the declared component between the page title and the outline, with zero SPA
patching. listPageTypes resolves defaultView to a defaultViewUrl; the SPA takes the
first namespace-matching declaration that has a view, imports it, and renders it with
{ page: { id, title, slug, metadata } } inside an error boundary, so a crash or a failed
load simply renders nothing rather than breaking the page. This is how a plugin gives a
whole directory of pages a consistent header card without touching core UI code.