Appearance
Plugin surfaces
Director ships exactly one plugin-style integration today: it dynamically imports the conversation surface from @open-matrix/chat-component/surface. This page documents what's actually happening and the architectural constraint that drives the design.
The shipped path: dynamic-import lazy chunk
Source: src/components/DirectorChatSurfaceHost.ts. The component class extends MatrixActorHtmlElement. On first activation of the Chat tab:
ts
this.loadState = 'loading';
this.renderLoading();
try {
await import('@open-matrix/chat-component/surface');
} catch (err) {
this.loadState = 'unavailable';
this.renderUnavailable();
return;
}
if (!customElements.get('conversation-surface')) {
this.loadState = 'unavailable';
return;
}
this.loadState = 'ready';
this.renderSurface();Vite resolves @open-matrix/chat-component/surface at build time and emits a separate JS chunk in Director's dist/. The browser fetches the chunk via a normal relative URL. No bare-specifier resolution failure, no import-map dependency.
Trade-offs (acknowledged in the source comment block):
- Pro: core class identity is preserved (single Vite build graph dedupes).
- Pro: initial-load JS in Director is unaffected; chunk is lazy.
- Pro: works in real browsers without a platform-feature dependency.
- Con: the chat-component surface code physically ships inside Director's dist. It is not "fetched from a registry at runtime". Truly external loading requires the import-map architecture below.
The genuine external-plugin path (target state)
WORKSTREAMS/package-structure/CORE-SOURCE-CONTAINMENT-HARVEST.md HARVEST-040 / EXPLOIT-M4-003 describe what real cross-package plugin loading needs:
- Host-served import map. The gateway publishes
/apps/<package>/import-map.jsondescribing the package's exports. - Browser import map. The gateway's bootstrap injects an import map into the page so
await import('/apps/chat-component/surface/register-surface.js')resolves at runtime instead of build time. - Class-identity discipline.
@open-matrix/coremust ship as a singleton on the platform's import map so two packages loading it independently get the same class.
None of that ships today. Director's lazy-chunk approach was the pragmatic choice that keeps the Chat tab working without those platform features.
What this means for new plugin surfaces
If you're adding a third surface (say, a flowpad-component surface), you have two options today:
- Bundle as lazy chunk. Add the package as a dependency of Director, import
'<package>/surface'from a*SurfaceHostcomponent, ship the resulting lazy chunk inside Director's dist. Same trade-offs as chat-component. - Wait for HARVEST-040. Don't add the dependency. The platform-level import-map work is the right-size fix.
Inline iframes are not a third option for Director surfaces — they break NATS context propagation and require their own bootstrap.
How chat-component/surface is structured
Source: projects/matrix-3/packages/chat-component/src/surface/register-surface.ts. It is intentionally narrow:
ts
import { ConversationSurfaceActor } from './ConversationSurfaceActor';
import { ConversationRenderer } from './ConversationRenderer';
import { ConversationInput } from './ConversationInput';
import { ConversationSessionList } from './ConversationSessionList';
export function registerConversationSurfaceElements(): void {
customElements.define('conversation-surface', ConversationSurfaceActor);
customElements.define('conversation-renderer', ConversationRenderer);
customElements.define('conversation-input', ConversationInput);
customElements.define('conversation-session-list', ConversationSessionList);
}
registerConversationSurfaceElements(); // side-effect on importSide-effect registration on module load means Director's await import() is sufficient to register the four custom elements; no extra call needed.
The Chat package's matrix.json declares the surface explicitly:
json
"surfaces": {
"conversation": {
"tag": "conversation-surface",
"module": "./dist/browser/surface/register-surface.js",
"route": "/apps/chat-component/surface/register-surface.js",
"description": "Embeddable conversation surface plugin loaded by host shells (Director, etc.) via dynamic import"
}
}The surfaces section is documentation/declaration for the future host-side import-map registry. It's not consumed by Vite directly; it's metadata for HARVEST-040.
Failure modes the host has to handle
DirectorChatSurfaceHost.ts handles three:
- Lazy chunk fetch fails (network, missing file): renders "unavailable".
- Module loads but
<conversation-surface>did not register: renders "unavailable". surface.set-target { mount }arrives before the surface loads: queue the mount inpendingTargetMount, dispatch on ready.
That defensive code exists because the host cannot rely on chunk-loading semantics being uniform across browsers and dev/prod builds.
See also
Source:
projects/matrix-3/packages/director/src/components/DirectorChatSurfaceHost.ts(full component, ~175 lines),projects/matrix-3/packages/chat-component/src/surface/register-surface.ts, andchat-component/matrix.json(surfacesblock).