Skip to content

UI architecture

Director follows a strict actor-composition pattern documented in the package's .claude/rules/webapp-compliance.md. Every page-level surface is a Matrix actor; every child is a Matrix actor; communication is op-based. This page is the developer reference for the rules.

Pattern A — shell IS the component

DirectorApp extends MatrixActorHtmlElement directly. The page-level shell is itself the page actor — there is no separate "page wrapper" class. From DirectorApp.ts:69:

ts
export class DirectorApp extends MatrixActorHtmlElement { ... }

The HTML host (<matrix-dsl-host> in index.html) wraps <director-app> and provides the runtime context. DirectorApp claims the actor identity and holds all state.

DirectorApp is the SOLE data fetcher

Every adapter call (DirectorRuntimeAdapter.*, DirectorDataAdapter.*) originates in DirectorApp. Children never call RequestReply.execute. This rule is enforced by the package's compliance file:

"DirectorApp is the sole data fetcher — tabs are pure projections, never call RequestReply directly."

Why: it keeps re-fetching predictable, lets DirectorApp cache and dedupe, and prevents children from independently reaching different runtime targets.

sendToChild() down, CustomEvent up

Downward: when DirectorApp needs to push data into a child, it uses sendToChild(name, op, payload). The op must appear in the child's static accepts. Example from DirectorExplorerShell.onExplorerSetTreeData:

ts
this.sendToChild('tree', 'tree.set-data', { roots: payload.roots ?? [] });

Upward: children dispatch CustomEvents on themselves with bubbles: true, composed: true. Their parent shell rebroadcasts them under a different name (e.g., director:node-selecteddirector:explorer-actor-selected). DirectorApp listens on the document for the rebroadcast and dispatches into its own accepts handler.

This is not ops-only. The downward path is op-based; the upward path is DOM-event-based. That asymmetry exists because the parent always knows where to send (it has child handles), but the child generally does not know where its parent listens.

Tabs are always mounted

DirectorDetail renders all seven detail tabs at all times and toggles display: none/block (DirectorDetail.ts:83-94):

css
.tab-content > * {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  overflow: auto;
  display: flex;
}

Tabs visible/hidden via class. Never create/destroy. Why: the actor lifecycle (subscribe/unsubscribe to bus, attach/detach to context) is expensive and lossy. Tabs that come and go would lose state on every flip.

Same rule for TabSessions's composed children: <director-session-list> and <director-session-chat> are always in the DOM, just sometimes hidden.

innerHTML for <director-*> elements is forbidden

The compliance rule:

NEVER use innerHTML to create <mx-*> or <director-*> elements — destroys actor lifecycle.

Why: MatrixActorHtmlElement.connectedCallback sets up the actor identity, registers ops, opens the inbox subscription. Replacing innerHTML triggers disconnection of the old actor and a fresh attach for the new one — but the new one starts with no state and no parent context. The result looks like flicker but is actually data loss.

Use static template (the framework parses it once and clones DOM nodes) or createElement + setAttribute('name', ...) + appendChild.

Methods on child actors are forbidden

The other half of the same rule:

NEVER call methods on child actors directly — use sendToChild(id, op, payload).

Direct method calls bypass the bus, skip introspection, and break with cross-process actors (children that may live on a different runtime). All inter-actor communication is op-based.

The runtime root rule

Children never read the global DOM for transport identity. The page actor (DirectorApp) computes runtime root via _resolveDaemonRoot() and DirectorTopology.resolveDirectorTopology() and passes it down explicitly. If DirectorTree needs to know "what's my root for display", it gets it via tree.set-root { root }, not by reading document.querySelector('matrix-dsl-host') itself.

Top-level Director surfaces are actors

Explorer, Workstreams, Ops Feed, and Search are not ad-hoc DOM swaps. Each is its own component:

director-explorer-shell
director-workstreams-shell
director-ops-feed
director-search-shell

DirectorApp chooses which one is display: block based on the active surface. Same persistence rules as tabs.

See also

Source: projects/matrix-3/packages/director/src/DirectorApp.ts, src/components/*.ts, and .claude/rules/webapp-compliance.md (package-local).