Workspace Composition Architecture
This document records the first-phase direction for turning Maya's Claude-like UI from a closed product shell into a developer-composable workspace platform.
The goal is not an end-user plugin marketplace. The first phase targets trusted developer modules installed at build time and imported explicitly by the host application. In this repository, the first-party Data Lab sample is consumed as a workspace-private package, for example pnpm --filter your-host add @maya/data-lab-module@workspace:*; external npm installability opens only when the package registry marks it npmPublishable.
North Star
The architecture follows four responsibilities:
Modules declare. Host resolves. Composer composes. Render runtime executes UI.
Expanded:
Module declares contract.
Host resolves runtime.
@maya/assistant-composition composes workspace.
@maya/assistant-ui renders composition through a scoped render runtime.
Module adapts to granted runtime.
Structure and execution stay separate:
Composition owns structure.
Render runtime owns executable UI.
Host wires both explicitly.
Convenience APIs may exist, but they must share the same deterministic pipeline:
One pipeline, two facades.
Convenience does not create hidden global state.
Non-Goals For Phase One
- No end-user plugin marketplace.
- No online runtime installation from a web UI.
- No untrusted third-party code sandbox.
- No iframe or micro-app plugin model.
- No pure JSON page schema as the primary module format.
- No module-owned persistent state container.
- No global module registry or import-time auto-registration.
- No arbitrary patching inside built-in pages.
- No grant, permission, configuration, storage, or health engine inside the UI package.
Package Boundaries
@maya/assistant-composition
This is the headless composition kernel.
It owns:
- module instance and resolved module instance contracts
- module contract metadata
- module runtime status and diagnostics types
- contribution descriptors
- typed opaque render handles
- composition policy
WorkspaceCompositioncomposeWorkspace()- conflict reports and ordering diagnostics
It must not own:
- React components
- DOM behavior
- CSS or token stylesheets
- Claude product assumptions
- host permission, deployment, storage, fetch, localStorage, or health checks
@maya/assistant-ui
This is the generic React renderer for a WorkspaceComposition.
It owns:
AssistantWorkspace- React render runtime types and helpers
- component primitives
- token system and scoped CSS foundations
- render-handle resolution into lazy React components
- ErrorBoundary, unknown-handle, preload, and fallback rendering behavior
It should not own Claude-specific built-in routes, product pages, or default Claude workspace composition.
@maya/claude-workspace
This is the Claude Web product kit. The current WCP-019 package root exports Claude built-in descriptors, the default policy, and createClaudeWorkspaceRuntime(). React entries, built-in render packs, CSS assets, npm publishability, and simple host resolution helpers remain deferred.
The canonical package boundary is .docs/packages/claude-workspace.md.
It owns:
- Claude-like built-in route contributions
- Claude-like sidebar, settings, customize, transcript, tool, and activity contributions
createClaudeWorkspaceRuntime()- future Claude-like render pack
- future Claude workspace CSS composition
- future high-level
ClaudeWorkspaceconvenience facade - future lower-level
ClaudeWorkspaceRenderer - future B-lite runtime helper for simple hosts
It depends on @maya/assistant-composition and @maya/assistant-ui/render-runtime.
@maya/*-module
A feature module package depends only on the layers it actually uses.
Typical modules depend on:
@maya/assistant-compositionfor headless declarations and contracts@maya/assistant-uionly when they ship React render packs or reuse primitives/tokens
Modules expose typed factories such as:
export function createDataLabModule(options: CreateDataLabModuleOptions): DataLabModuleInstance;
export function bindDataLabModuleRuntime(
module: DataLabModuleInstance,
runtime: DataLabRuntimeDefinition,
): ResolvedModuleInstance;
They expose CSS through explicit subentries:
import '@maya/data-lab-module/styles.css';
import { createDataLabModule } from '@maya/data-lab-module';
Module CSS must be scoped under .aui-root and module-specific class prefixes. It must not reset global body, button, input, :root, or host application selectors.
Module Instance Model
The system is modeled around module instances, not packages.
packageId identifies code origin. instanceId identifies one enabled instance in a workspace.
The canonical ResolvedModuleInstance and module factory shapes live in
workspace-composition-module-authoring.md and the @maya/assistant-composition
public type contract. This overview only records ownership semantics, so it must
not duplicate the full source type.
All contributions are namespaced by instanceId.
Module-internal IDs are local:
routes: [{ id: 'home', path: '/data-lab' }]
configurationSurfaces: [{ id: 'connection', ... }]
The composition kernel turns them into canonical keys such as:
data-lab-prod:routes/home
data-lab-prod:configuration-surfaces/connection
Default conflict behavior is fail-fast:
- duplicate
instanceIdis an error - duplicate route path is an error
- duplicate renderer claim is an error
- duplicate canonical contribution key is an error
The host may explicitly resolve conflicts through policy, aliasing, route-path changes, contribution disabling, or explicit override. Import order must never cause implicit replacement.
Multiple instances of the same package are first-class:
const stagingData = createDataLabModule({
instanceId: 'data-lab-staging',
routePath: '/data-lab-staging',
label: 'Data Lab Staging',
});
const prodData = createDataLabModule({
instanceId: 'data-lab-prod',
routePath: '/data-lab',
label: 'Data Lab',
});
Module State Ownership
Modules are UI adapters for host capabilities. They are not state containers.
Host-owned:
- persistent configuration
- credentials and secrets
- tenant policy
- user permissions
- business data
- network requests
- storage
- audit behavior
- cross-refresh or synchronized state
Module factory-owned input:
- typed configuration and controller callbacks provided by the host
Module component-owned:
- transient UI state only, such as active tab, expanded row, temporary input, popover open state, and local visual affordances
Any state that needs persistence, synchronization, auditing, or permission control must be lifted to the host and passed back through the module's typed controller.
AssistantWorkspace must not introduce a generic moduleState: Record<string, unknown> escape hatch.
Module Contract And Runtime
Modules declare contracts. Hosts resolve runtime.
A module contract describes the module's capabilities, grants, permissions, configuration requirements, host-owned storage requests, supported runtime statuses, and fallback semantics. The canonical ModuleContract shape lives in workspace-composition-module-authoring.md and in the @maya/assistant-composition public type contract.
The host uses the contract plus deployment capabilities, tenant policy, user permissions, module configuration, and backend health to produce resolved module runtime:
The canonical ResolvedModuleRuntime shape lives in
workspace-composition-module-authoring.md and the @maya/assistant-composition
public type contract. Its v0 statuses are ready, readOnly,
needsConfiguration, unauthorized, disabled, and error; missing is a
string list and actions is a list of RuntimeAction descriptors.
Runtime resolution is host-owned. Neither @maya/assistant-ui nor @maya/assistant-composition decides whether a user has a permission, whether an API key is valid, or whether a backend service is healthy.
The composition kernel consumes already resolved ResolvedModuleInstance
descriptors from the @maya/assistant-composition public type contract.
Modules adapt to their granted runtime and render ready, disabled, needs-configuration, unauthorized, read-only, or error states through explicit fallback surfaces.
Contributions
Phase one supports high-cohesion extension points:
- routes
- sidebar items and sections
- configuration surfaces
- integration surfaces
- message renderers for module-owned custom messages
- tool renderers for claimed tool names
- activity renderers for claimed activity kinds
- before/after message appenders
- fallback surfaces for module runtime states
Phase one supports:
- adding pages
- hiding built-in pages
- reordering navigation
- modeling built-in replacement in policy and diagnostics
Phase one does not publicly support:
- replacing built-in page implementations
- patching arbitrary panels inside built-in pages
- replacing default user or assistant text message rendering
Whole-route replacement is a reserved architecture path because it is more cohesive and less coupled than local DOM patching. It should not be published until route context and ownership semantics are stable.
Configuration And Integration Surfaces
Modules should not hard-code where their settings appear in the UI.
They declare surfaces by semantic purpose:
- configuration surfaces: API keys, base URLs, permissions, defaults, model choices, administrator or developer configuration
- integration surfaces: connection status, enablement status, entry points, module summary cards, management actions
The product kit maps these surfaces into Settings, Customize, Admin Console, or other host surfaces. This keeps module APIs stable if the Claude workspace layout changes.
The current source-level migration map for Claude UI surfaces is workspace-composition-surface-migration-map.md. That WCP-014 map is the canonical place for route, sidebar, settings, customize, transcript, tool, activity, fallback, unsupported surfaces, and first safe visual parity milestone details.
Message And Renderer Policy
Phase one renderer rules:
- Modules may render custom message types they declare.
- Modules may render tool cards for tool names they claim.
- Modules may render activity cards for activity kinds they claim.
- Modules may append cards before or after default message content.
- Modules may not replace ordinary default user or assistant text rendering.
Future API may allow explicit override of default rendering, but it should be opt-in, policy-gated, and separate from the first-phase renderer contract.
Workspace Composition
composeWorkspace() compiles built-ins, resolved modules, and policy into WorkspaceComposition.
const composition = composeWorkspace({
builtins,
modules: resolvedModules,
policy,
});
It owns:
- route, sidebar, configuration surface, integration surface, message renderer, tool renderer, and activity renderer merging
- module instance namespace
- ordering policy
- route path conflict detection
- renderer claim conflict detection
- explicit v0 policy operations:
hide,reorder,replace,pathAlias, andrendererOverride - fallback surface selection
- diagnostics and error reports
- disabled, unauthorized, needs-configuration, read-only, and error composition entries
The replace and rendererOverride operations are descriptor-level composition policy only. They do not publish a built-in page replacement API, local DOM patching model, or page-internal slot system in v0.
It does not own:
- grant or runtime resolution
- permission decisions
- deployment configuration
- backend health checks
- fetch or storage
- localStorage
- React rendering
- DOM or CSS
Render Handles And Render Runtime
WorkspaceComposition contains typed opaque render handles. It does not contain React components.
The canonical RenderHandle shape lives in
workspace-composition-kernel.md and the @maya/assistant-composition public
type contract. It contains renderHandleVersion, kind, id, and optional
JSON-only data; ownership, trust tier, preload, and component loading belong
to composition output or the scoped React render runtime, not to the handle.
The composition kernel validates render claims and structure, but it never imports React or executes render functions.
The host explicitly creates a scoped React render runtime:
const renderRuntime = createReactRenderRuntime({
packs: [
hostProvidedClaudeRenderPack,
dataLabRenderPack,
],
trustPolicy,
preloadPolicy,
});
renderRuntime owns:
- handle-to-component resolution
- lazy loading
- route and intent preload
- ErrorBoundary behavior
- unknown-handle diagnostics
- trust policy enforcement
- fallback rendering
No global render registry or import-time auto-registration is allowed in phase one.
Claude Workspace Product Entry
After WCP-015 and WCP-018 made the render runtime and composition renderer executable, WCP-019 adds the first product-kit entry through createClaudeWorkspaceRuntime().
Current builder use:
const runtime = createClaudeWorkspaceRuntime({
modules: resolvedModules,
policy,
renderPacks: [hostProvidedClaudeRenderPack, dataLabRenderPack],
});
Lowest-level renderer use:
<AssistantWorkspace
{...workspaceControllers}
composition={runtime.composition}
renderRuntime={runtime.renderRuntime}
/>
The React facade snippets below are future target shapes, not current runnable imports.
High-level facade:
<ClaudeWorkspace
controllers={controllers}
modules={resolvedModules}
policy={policy}
renderPacks={[dataLabRenderPack]}
/>
It internally calls the same deterministic builder:
const runtime = createClaudeWorkspaceRuntime({
modules: resolvedModules,
policy,
renderPacks,
});
Lower-level facade:
const runtime = createClaudeWorkspaceRuntime({
modules: resolvedModules,
policy,
renderPacks,
});
<ClaudeWorkspaceRenderer
{...workspaceControllers}
composition={runtime.composition}
renderRuntime={runtime.renderRuntime}
/>
All facades must use one composition/render pipeline. Convenience components cannot create hidden global state.
B-Lite Runtime Helper
@maya/claude-workspace may later provide a minimal helper for simple hosts:
const resolvedModules = resolveClaudeWorkspaceModules({
moduleInstances: [dataLabModule],
runtime: {
'data-lab-prod': {
status: 'ready',
grants: ['data-lab.query'],
missing: [],
actions: [],
},
},
});
This helper is intentionally thin. It can validate shape, instance IDs, and status coverage, but it must not pretend to understand deployment policy, tenant permissions, configuration stores, or health checks.
Migration Stages
Stage 0: Documented Architecture
- Add this architecture document.
- Keep current UI implementation unchanged.
- Add
workspace-composition-surface-migration-map.mdso current source-level surfaces have one canonical descriptor migration map before code moves. - Align package docs and verification docs around the future package boundary without publishing new APIs yet.
Stage 1: Headless Composition Package Skeleton
- Add
@maya/assistant-composition. - Define headless types, render handles, policy, diagnostics, and
WorkspaceComposition. - Add unit tests for namespace generation, ordering, conflict detection, and diagnostics.
- No React dependency.
Stage 2: Claude Built-ins As Contributions
- Define the
@maya/claude-workspacepackage boundary and model Claude built-ins as contributions. - Move current built-in route/sidebar/settings/customize/transcript contribution descriptions into Claude workspace built-ins.
- Keep React render implementations in the existing UI source until render-pack extraction is ready.
Stage 3: Render Runtime
- Add React render runtime in
@maya/assistant-ui. - Introduce generic render packs and handle resolution for module and host-provided UI.
- Keep Claude built-in render-pack extraction and
@maya/claude-workspacerender-pack export as post-v0 product-kit work. - Add preload, unknown-handle diagnostics, and ErrorBoundary behavior.
Stage 4: Composition-Based Workspace Rendering
- Change
AssistantWorkspaceto renderWorkspaceCompositionplus scopedrenderRuntime. - Replace hard-coded route switches with composed route rendering.
- Keep controller ownership host-owned.
- Update Storybook to show composition scenarios and diagnostics states.
Stage 5: Developer Module Path
- Create one first-party module package as a proving ground.
- Use a typed factory, explicit CSS subentry, module contract, resolved runtime, contribution descriptors, and render pack.
- Verify multi-instance behavior.
- Verify module route, surface, renderer, fallback, and conflict diagnostics.
Stage 6: Product Facades
- Document the current default product-kit path through
createClaudeWorkspaceRuntime(...)and explicitAssistantWorkspacecomposition/render-runtime wiring. - Keep
ClaudeWorkspace,ClaudeWorkspaceRenderer, Claude built-in render packs, CSS assets, and B-lite resolution helper as future convenience APIs until their contracts are explicit. - Keep low-level composition and UI renderer APIs available for advanced hosts.
Closed And Remaining Decisions
The v0 diagnostic registry, CompositionPolicy JSON shape, AssistantWorkspaceCompositionInput controller shape, package registry classification, and grant semantics are no longer open architecture questions; their canonical homes are the namespace/diagnostics spec, composition kernel spec, assistant-ui package guide, release governance, and security trust model.
The remaining post-v0 decisions are narrower:
- future closed/extended registry for
RenderHandle.kindvalues beyond the current contribution-to-handle mapping - SSR contract for
ReactRenderRuntime - public built-in route replacement timing and route context shape after v0
- future untrusted external module architecture, including marketplace, sandbox, remote loading, and permission enforcement