Skip to main content

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
  • WorkspaceComposition
  • composeWorkspace()
  • 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 ClaudeWorkspace convenience 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-composition for headless declarations and contracts
  • @maya/assistant-ui only 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 instanceId is 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, and rendererOverride
  • 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.md so 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-workspace package 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-workspace render-pack export as post-v0 product-kit work.
  • Add preload, unknown-handle diagnostics, and ErrorBoundary behavior.

Stage 4: Composition-Based Workspace Rendering

  • Change AssistantWorkspace to render WorkspaceComposition plus scoped renderRuntime.
  • 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 explicit AssistantWorkspace composition/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.kind values 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