Appearance
Runtime integration
Chat ships three runtime modes, declared at chat/src/runtime/resolve-chat-config.ts:9:
ts
type TChatRuntimeMode = 'standalone-local' | 'standalone-connected' | 'daemon-hosted';Each mode picks a different set of service implementations and a different transport.
standalone-local
Self-contained. No network, no NATS, no other actor. Used in tests, demos, and the docs sandbox.
create-standalone-local-chat.ts:
ts
const config = await resolveConfiguredChatConfig({ ...overrides, mode: 'standalone-local' });
const stateStore = new LocalChatStateStore();
const runtime = createChatRuntime({ config, services, logging: false });
const boundServices = {
conversation: services.conversation ?? new LocalConversationService(runtime.getRootContext(), stateStore),
componentLibrary: services.componentLibrary ?? new LocalComponentLibraryService(),
identity: services.identity ?? new LocalIdentityService({ authenticated: true, principalId: 'local-user', ... }),
preferences: services.preferences ?? new LocalPreferencesService(),
sessionState: services.sessionState ?? new LocalSessionStateService(stateStore),
mcp: services.mcp ?? new NullMcpStatusService(),
};
bindChatServices(runtime, config, boundServices);
const app = await runtime.createElement<HTMLElement>('chat-app', { mount: config.mount, name: config.app.appName });
app.setAttribute('runtime-mode', config.mode);
await runtime.mount(app);The Local* implementations live in chat/src/app/services/impl/. They keep state in a single LocalChatStateStore (in-memory + optional IndexedDB).
standalone-connected
Browser-side, but with a caller-supplied ITransportAdapter (typically NATS-WS to a remote bus). Useful when embedding Chat into another app's runtime that already owns a transport.
create-standalone-connected-chat.ts:
ts
const config = await resolveConfiguredChatConfig({ ...overrides, mode: 'standalone-connected' });
const runtime = createChatRuntime({ config, services, transport, logging: false });
const app = await runtime.createElement('chat-app', { mount: config.mount, name: config.app.appName });
app.setAttribute('runtime-mode', config.mode);
await runtime.mount(app);The caller is responsible for binding services. Typically the caller supplies RemoteActorConversationAdapter, RemoteActorComponentLibraryAdapter, etc., pointed at appropriate bus mounts.
daemon-hosted
The default mode for /apps/chat/ on a real Host. The runner (Host Service) provides the MatrixRuntime and Chat just adds its actors.
create-host-managed-chat.ts:
ts
const runtime = services.runtime;
if (!runtime) throw new Error('createHostManagedChat requires a runner-provided MatrixRuntime service.');
const config = await resolveConfiguredChatConfig({
...overrides,
mode: 'daemon-hosted',
...(bootstrapContext.mount ? { mount: bootstrapContext.mount } : {}),
...(bootstrapContext.root ? {
transport: { ...overrides.transport, root: bootstrapContext.root, addressRoot: bootstrapContext.root, daemonRoot: bootstrapContext.root },
} : {}),
});
const boundServices = {
componentLibrary: services.componentLibrary ?? new LocalComponentLibraryService(),
identity: services.identity ?? new LocalIdentityService({ authenticated: true, ... }),
preferences: services.preferences ?? new LocalPreferencesService(),
mcp: services.mcp ?? new NullMcpStatusService(),
// conversation and sessionState come from the runner if provided
};
bindChatServices(runtime, config, boundServices);
const mounted = await mountHostManagedChatActors(runtime, mount);
return { runtime, config, mounts: mounted };Note: in daemon-hosted mode, LocalConversationService is not bound by default. The runner is expected to provide a RemoteActorConversationAdapter that points at system.agents. The browser side then receives bound config via bindChatServices.
mountHostManagedChatActors mounts:
chat (ChatRootActor)
chat.security-realm (ChatSecurityRealmService)
chat.topic-claims (ChatTopicClaimService)
chat.identity (ChatIdentityProxy)
chat.conversation (ChatConversationProxy)
chat.component-library (ChatComponentLibraryProxy)
chat.preferences (ChatPreferencesProxy)
chat.session-state (ChatSessionStateProxy)
chat.mcp (ChatMcpProxy)
chat.export (ChatExportService)Each is mounted via runtime.createSupervised(ActorClass, mount) so the supervisor restarts them if they crash.
Mode resolution at boot
resolve-chat-config.ts:110-115:
ts
export function inferChatMode(overrides?: DeepPartial<IChatRuntimeConfig>): TChatRuntimeMode {
if (overrides?.mode) return overrides.mode;
throw new Error(
'[MatrixChat] Runtime mode must be explicit. Pass mode directly or bootstrap chat through an explicit standalone/hosted entrypoint.',
);
}The mode is never inferred from the environment. Either you pass mode in the overrides or you call one of the explicit create* entrypoints. There is no auto-detection that switches between local and hosted at runtime.
Service binding
bind-chat-services.ts exposes:
bindChatServices(runtime, config, overrides)— registers the config and overrides on the runtime so the browser-side proxies can find them.getBoundChatConfig(context)— used by ChatApp at boot.getBoundChatServiceOverrides(context)— same, for service overrides.resolveChatServiceTarget(context, serviceRef)— turn anIChatServiceRefinto an actual mount.
The browser side reads these from runtime.getService(CHAT_CONFIG_SERVICE_KEY) and friends. If unbound, ChatApp throws on first access — there is no silent default.
See also
Source:
projects/matrix-3/packages/chat/src/runtime/directory.