Appearance
Surface registration
projects/matrix-3/packages/chat-component/src/surface/register-surface.ts is the single registration entrypoint. This page documents what it does, why it does it that way, and what hosts must (and must not) do.
What the file is
ts
import { ConversationSurfaceActor } from './ConversationSurfaceActor';
import { ConversationRenderer } from './ConversationRenderer';
import { ConversationInput } from './ConversationInput';
import { ConversationSessionList } from './ConversationSessionList';
export function registerConversationSurfaceElements(): void {
if (typeof customElements === 'undefined') return;
const definitions: Array<[string, CustomElementConstructor]> = [
['conversation-surface', ConversationSurfaceActor],
['conversation-renderer', ConversationRenderer],
['conversation-input', ConversationInput],
['conversation-session-list', ConversationSessionList],
];
for (const [tag, ctor] of definitions) {
if (!customElements.get(tag)) {
customElements.define(tag, ctor);
}
}
}
registerConversationSurfaceElements(); // side-effect on module loadif (!customElements.get(tag)) is the idempotency guard. A second import is a no-op.
Why side-effect on import
Hosts can register with a single dynamic import:
ts
await import('@open-matrix/chat-component/surface');
// At this point all four custom elements are registered.If registration weren't a side-effect, every host would need:
ts
const mod = await import('@open-matrix/chat-component/surface');
mod.registerConversationSurfaceElements();That's two operations and an opportunity to forget the second. The side-effect convention is consistent with how Chat's own register-elements.ts self-registers (line 64).
Class identity guarantee
The four classes extend MatrixActorHtmlElement from @open-matrix/core. For the lazy-chunk pattern to work, the host must use the same @open-matrix/core instance as the surface. Otherwise instanceof checks fail and the actor system rejects the bus connection.
The Vite resolution rule that protects this:
- Director's
vite.config.tsaliases@open-matrix/core/*to source. - chat-component's
tsconfig.jsonreferences the same@open-matrix/coreworkspace package. - Vite walks both, sees the same source path, and emits a single copy of the core classes in the bundle.
If anything breaks that alignment (dual installation of @open-matrix/core, two Vite builds with different resolution), customElements.define may throw "tag already defined" with a class that has the same shape but different identity. Diagnosing those is painful; the prevention is to keep @open-matrix/core resolution unified.
What a host must do
Minimum:
ts
class MyHost extends MatrixActorHtmlElement {
async ensureSurface() {
await import('@open-matrix/chat-component/surface');
// Now create the element.
this.shadowRoot!.innerHTML = '<conversation-surface name="conv"></conversation-surface>';
}
}Better (matches Director's DirectorChatSurfaceHost):
- Track loadState (
idle | loading | ready | unavailable). - Catch import failures and render an unavailable message with rebuild hint.
- Queue any
surface.set-target { mount }ops that arrive before ready. - Forward target-set ops via DOM event after registration.
What a host must NOT do
- Import the surface statically (
import '@open-matrix/chat-component/surface'). That would pull the surface into the host's main bundle, defeating the lazy-loading benefit and (more importantly) hard-linking class identity to whatever the host's resolution graph looked like at build time. - Re-define the four custom elements with different classes. The first registration wins; trying to register
conversation-surfaceagain with a different class throws. - Bypass
customElements.defineand use the class directly. The actor lifecycle (context, mailbox subscription,connectedCallback) requires DOM registration.
Per-element interfaces
ConversationSurfaceActor (<conversation-surface>):
- Accepts:
surface.set-target { mount },surface.set-session { sessionId },surface.clear { options? }. - Emits:
conversation:target-changed { mount },conversation:session-changed { sessionId },conversation:error { message }.
ConversationRenderer (<conversation-renderer>):
- Accepts:
renderer.add-message,renderer.set-messages,renderer.stream-thinking,renderer.stream-append,renderer.add-tool,renderer.add-tool-result,renderer.stream-end,renderer.clear.
ConversationInput (<conversation-input>):
- Accepts:
input.enable,input.disable,input.focus,input.set-placeholder. - Emits:
conversation:submit { text }.
ConversationSessionList (<conversation-session-list>):
- Accepts:
sessions.set-list { sessions },sessions.set-active { sessionId }. - Emits:
conversation:session-selected { sessionId },conversation:session-new,conversation:sessions-refresh-requested.
See also
Source:
projects/matrix-3/packages/chat-component/src/surface/register-surface.ts(full file), eachConversation*.ts(accepts/emits).