with one click
composer-plugins
// Use when working on files in packages/plugins/, adding new plugins, refactoring plugin components/containers, writing storybooks for plugins, or wiring capabilities like react-surface or operation-resolver.
// Use when working on files in packages/plugins/, adding new plugins, refactoring plugin components/containers, writing storybooks for plugins, or wiring capabilities like react-surface or operation-resolver.
Land an existing PR โ finds it, fixes CI failures iteratively, keeps the branch up to date with main, subscribes to PR events for continuous autofixing, and adds to merge queue. Use when the user says "/land <PR number or URL>" or asks to land/ship an existing PR. Accepts optional extra instructions after the PR reference.
Test assistant conversations, agents, and blueprints using AssistantTestLayer, Effect/vitest, ECHO types, and memoized LLM fixtures. Use when writing or fixing assistant-toolkit tests, blueprint.operation tests, AiSession flows, or when CI fails on missing memoized conversations.
Author DXOS Composer plugins โ primarily community plugins built in their own repo (Vite + composerPlugin, GitHub release, registered via dxos/community-plugins), with notes on how the in-repo workflow differs. Use when scaffolding a new Composer plugin, wiring capabilities (surfaces, operations, blueprints), exposing operations to AI agents, integrating external services, testing with the composer testing harness, or publishing to the community registry.
Use when authoring or refactoring Radix-style composite React components in `@dxos/react-ui` and sibling UI packages โ namespaced primitives like `Foo.Root` / `Foo.Trigger` / `Foo.Content` built around `forwardRef`, `Slot`, and a `tx()` theme function.
Guides working with Effect-TS in TypeScript codebases. Use when writing Effect programs, defining services/layers, handling errors, running effects, or when code uses effect, Context, Layer, Effect.gen, or related Effect patterns.
Reference for `SubductionPolicy` (the four hooks `authorizeConnect`, `authorizeFetch`, `authorizePut`, `filterAuthorizedFetch` passed via `Subduction.hydrate(..., policy)` or `new Repo({ subductionPolicy })`). Use when designing client-side access control over Subduction-replicated data, choosing which hook to deny in, or debugging why a doc did or did not replicate.
| name | composer-plugins |
| description | Use when working on files in packages/plugins/, adding new plugins, refactoring plugin components/containers, writing storybooks for plugins, or wiring capabilities like react-surface or operation-resolver. |
Exemplar: packages/plugins/plugin-chess. Read its source files to understand every pattern below.
Use the dxos-introspect MCP server (@dxos/introspect-mcp, served by the dx-introspect-mcp binary) as the source of truth for plugin metadata and reference examples โ not directory listings.
A "plugin" is a package whose src/meta.ts exports a Plugin.Meta, so ls packages/plugins/ overcounts (e.g. plugin-generator is tooling, not a plugin).
mcp__dxos-introspect__list_plugins โ enumerate plugins (filter by id substring; pass compact: true for identifying fields only).mcp__dxos-introspect__get_package โ package details for a given plugin.mcp__dxos-introspect__list_surfaces / list_capabilities / list_operations / list_schemas โ drill into a plugin's contributions.mcp__dxos-introspect__find_symbol / get_symbol / list_symbols โ locate code by symbol rather than grepping paths.mcp__dxos-introspect__list_idioms โ enumerate @idiom-tagged reference examples (filter by slug substring or hostKind: 'symbol' | 'story' | 'test').Reach for these first when answering questions like "how many plugins", "which plugin contributes X surface", or "where is symbol Y defined".
Required. Before writing or refactoring any container, capability, operation, blueprint, or schema, call mcp__dxos-introspect__list_idioms and scan for a slug that matches what you're about to build. An idiom is a JSDoc-tagged pinning of the canonical way to do one thing โ when one exists, it is the answer, and you should get_symbol on the host artifact and follow the pattern rather than reinventing it.
Typical triggers:
org.dxos.react-ui-menu.* idioms.useObject / mutating ECHO subjects โ look for ECHO idioms.If no idiom matches, proceed using the exemplar (plugin-chess); if you find yourself writing something that other plugins will copy, consider adding a new @idiom tag (see packages/reflect/deus/docs/IDIOMS.md for the format and slug rules).
Each plugin MUST have a PLUGIN.mdl specification written in the MDL (.mdl) language defined by @dxos/deus. The authoritative references live under packages/reflect/deus/:
docs/DESIGN.md โ language specification.docs/IDIOMS.md โ idiom format and @idiom JSDoc-tag conventions.lang/core.mdl โ core dialect.lang/PLUGIN-.template.mdl โ the plugin template.src/extension/mdl.grammar โ Lezer grammar (use only when chasing syntax questions).The PLUGIN.mdl IS the design document. Do not write a separate design doc (e.g., in agents/superpowers/specs/). During brainstorming, once the design is approved, write the spec directly as packages/plugins/plugin-<name>/PLUGIN.mdl. Use packages/reflect/deus/lang/PLUGIN-.template.mdl as the template and packages/plugins/plugin-chess/PLUGIN.mdl as a reference.
The specification is the source of truth for what the plugin does. It must be:
feat, req, and test blocks.When the user discusses new features or changes, update PLUGIN.mdl to reflect the agreed requirements before implementing.
Tests should verify the behaviors described in the spec.
/superpowers:writing-plans (Subagent-Driven) for non-trivial plugin work.When asked to create a new plugin, start with a minimal skeleton before adding features. The skeleton should include:
PLUGIN.mdl โ specification starter with initial feature/requirement blocks.README.md โ brief description of the plugin's purpose.package.json โ with "private": true, #imports aliases, and minimal dependencies.moon.yml โ with compile entry points.src/meta.ts โ plugin metadata (id, name, description, icon, iconHue).src/translations.ts โ initial translation resources.src/FooPlugin.tsx โ minimal Plugin.define(meta).pipe() with surface and translations modules.src/index.ts โ exports only meta and plugin.src/types/ โ one schema type with make() factory.src/capabilities/index.ts โ single Capability.lazy() for ReactSurface.src/capabilities/react-surface.tsx โ one surface for the article role.src/containers/ โ one container (e.g., FooArticle) with lazy export and basic storybook.src/components/ โ empty barrel, ready for primitives.Build and lint the skeleton before adding features.
Add capabilities incrementally as needed (operations, blueprints, settings, etc.).
Register the plugin with composer-app.
plugin-foo/
package.json
moon.yml
PLUGIN.mdl
src/
index.ts # Root entrypoint; exports only the plugin and meta.
meta.ts # Plugin.Meta (id, name, description, icon, iconHue).
translations.ts # i18n resources keyed by typename and meta.id.
FooPlugin.tsx # Plugin definition via Plugin.define(meta).pipe().
blueprints/ # AI blueprint definitions.
index.ts
capabilities/ # Lazy capability modules (one file each).
index.ts # Barrel of Capability.lazy() exports.
react-surface.tsx
operation-handler.ts
blueprint-definition.ts
components/ # Primitive UI components (no app-framework deps).
index.ts
MyComponent/
index.ts
MyComponent.tsx
MyComponent.stories.tsx
containers/ # Surface components (lazy-loaded, use capabilities).
index.ts # lazy(() => import('./X')) exports.
FooArticle/
index.ts # Bridges named -> default export.
FooArticle.tsx
FooArticle.stories.tsx
operations/ # Operation definitions and handlers.
index.ts
definitions.ts
types/ # ECHO schema definitions.
index.ts # Namespace re-export: export * as Foo from './Foo';
Foo.ts
src/components/)Low-level UI. Must NOT depend on @dxos/app-framework or @dxos/app-toolkit.
Each component lives in its own subdirectory with an index.ts barrel.
Use named exports; no default exports. Create a basic storybook for each.
Prefer composable Radix-style namespaces for non-trivial components. Mirror the Foo.Root / Foo.Toolbar / Foo.Content / Foo.Viewport pattern used by Panel.*, Card.*, Masonry.*, and ScrollArea.* in @dxos/react-ui and @dxos/react-ui-masonry. The Root provides shared context (data, callbacks, Tile component); subcomponents read it and slot into the outer Panel/ScrollArea structure. This lets containers plug in their own toolbar contents (e.g. MenuBuilder buttons) without forking the component, and keeps the component fully presentation-only.
// Pure component namespace โ no app-framework deps.
export const FooMasonry = { Root: Root, Toolbar: Toolbar, Content: Content, Viewport: Viewport };
// Container composes:
<FooMasonry.Root items={items} onDelete={handleDelete}>
<FooMasonry.Toolbar>
<Menu.Root {...menuActions} attendableId={attendableId}>
<Menu.Toolbar />
</Menu.Root>
</FooMasonry.Toolbar>
<FooMasonry.Content>
<FooMasonry.Viewport />
</FooMasonry.Content>
</FooMasonry.Root>;
Sketch the namespace export first when designing a new component; only collapse to a single component if the surface really has no slots.
See: plugin-chess/src/components/Chessboard/, packages/ui/react-ui-masonry/src/Masonry.tsx
src/containers/)High-level surface component. Uses capabilities and is referenced by react-surface.
Each container lives in its own subdirectory. The subdirectory index.ts bridges named to default export (for React.lazy).
The top-level containers/index.ts uses lazy(() => import('./X')) with : ComponentType<any> annotation.
Surface components use suffixes matching their role: Article, Card, Dialog, Popover, Settings.
Create a basic storybook for each.
If a "component" needs useCapability/useCapabilities/useAppGraph/useOperationInvoker, it belongs in containers/. Storybooks won't have a PluginManager โ calling capability hooks under components/ throws. Refactor: take the resolved value (URL, callback, Tile component) as a prop and move the hook one level up.
useObjectA surface receiving an ECHO subject via AppSurface.ObjectArticleProps<T> MUST call useObject(subject) and read from the returned snapshot. Without it, mutations to nested arrays/structs (e.g. Obj.update(obj, m => m.images = [...])) do not trigger re-render until you navigate away and back โ the prop reference stays stable; the subscription lives in useObject.
const [gallery] = useObject(subject);
// reads (gallery.images) re-render reactively
// writes still go through the original subject:
const handleDelete = (i: number) =>
Obj.update(subject, (obj) => {
const m = obj as Obj.Mutable<Gallery.Gallery>;
m.images = (m.images ?? []).filter((_, idx) => idx !== i);
});
The snapshot type is narrow โ cast as needed (obj as Obj.Mutable<T> inside Obj.update, or as T for read access of fields not surfaced on Snapshot<T>).
NEVER hand-roll native form controls (<textarea>, <input>, <select>) in a plugin. They
don't inherit the theme โ a bare <textarea> renders as a white box in dark mode โ and they
bypass validation. Two rules:
Edit ECHO objects with Form + schema, not raw inputs. Render
<Form.Root schema={Type.getSchema(Foo)} values={obj} autoSave onSave={...}> from
@dxos/react-ui-form and let it generate inputs from the Effect Schema โ it handles strings,
numbers, booleans, enums (Schema.Literal / Format), nested Schema.Struct, Schema.Array,
and Schema.Record. Hide non-editable fields with FormInputAnnotation.set(false). For a field
that needs a bespoke editor, register it via the Form's fieldMap / fieldProvider (see
plugin-kanban KanbanSettings) โ never a native element. If you must edit an opaque
document (e.g. a stored JSON Schema), model it with typed sub-schemas (the mapping structs are
already Effect Schemas โ render request/result as nested form fields) rather than dropping
to a <textarea>.
Never invent Tailwind color tokens. bg-input and text-primary are NOT valid tokens and
render wrong (e.g. white-on-white). Use the themed @dxos/react-ui primitives (Input.*,
Card.*, Button, IconButton) or real semantic tokens from @dxos/react-ui-theme
(text-baseText, bg-base, bg-modalSurface, text-description, text-subdued,
border-separator, โฆ). When unsure, copy classes from an existing themed component instead of
guessing, and pass class arrays to classNames on react-ui components rather than styling raw
elements.
asChild composabilityCard.Header and Card.Row are 3-slot subgrids (grid-cols-subgrid: leading icon ยท content 1fr ยท trailing action). Children are placed by ORDER. A lone <Card.Title> as the only child lands in the narrow leading icon slot and gets clamped (e.g. a title renders as "20โฆ"). Put real content in the CENTRE slot: bracket it with the icon slots, or wrap the content in a single element that occupies slot 2:
<Card.Header>
<Card.IconBlock /> {/* slot 1 (icon) โ empty placeholder */}
<div className='flex flex-col gap-0.5 min-w-0'>
{' '}
{/* slot 2 (1fr content) */}
<Card.Title classNames='line-clamp-2'>{title}</Card.Title>
{price && <span className='text-sm text-description'>{price}</span>}
</div>
<Card.IconBlock /> {/* slot 3 (action) โ empty placeholder */}
</Card.Header>
A component used as the child of Focus.Item asChild (or any Radix Slot/asChild) MUST be composable โ a single element that forwards ref and spreads injected props. A plain function component silently drops the Slot's ref/handlers, so current/keyboard/click wiring never attaches. Make presentational cards forwardRef and spread:
export const FooCard = forwardRef<HTMLDivElement, FooCardProps>(({ subject, current, classNames, ...props }, ref) => (
<Card.Root ref={ref} classNames={['dx-hover', current && 'dx-current', classNames]} {...props}>
โฆ
</Card.Root>
));
// then: <Focus.Item asChild current={current} onCurrentChange={โฆ}><FooCard subject={x} current={current} /></Focus.Item>
MenuBuilder + useMenuActions + attendableIdAlways thread attendableId from AppSurface.ObjectArticleProps into <Menu.Root>. Don't underscore it as unused โ without it, attention-driven contributions don't target the right surface.
const actionsAtom = useMemo(
() =>
Atom.make(
(): ActionGraphProps =>
MenuBuilder.make()
.action(
'add',
{ label: ['add.label', { ns: meta.id }], icon: 'ph--plus--regular', disposition: 'toolbar' },
handleAdd,
)
.build(),
),
[handleAdd],
);
const menuActions = useMenuActions(actionsAtom);
return (
<Panel.Toolbar>
<Menu.Root {...menuActions} attendableId={attendableId}>
<Menu.Toolbar />
</Menu.Root>
</Panel.Toolbar>
);
See: plugin-sample/src/containers/SampleArticle.tsx.
Containers must use standard UI primitives โ never custom classNames for layout or styling. Use:
Panel.Root / Panel.Toolbar / Panel.Content for container (article, companion, etc.) layout structure.ScrollArea.Root + ScrollArea.Viewport inside Panel.Content asChild for scrollable content.Input.Root / Input.Label / Input.TextInput for form fields.Button (with variant) for actions.Clipboard.IconButton for copy-to-clipboard.Toolbar.Root / Toolbar.IconButton for toolbar actions.Card.Root / Card.Toolbar / Card.Content for card surfaces.List.Root for navigable lists that track current (dx-current) and selected (dx-selected) item states.react-tabster for navigation.IMPORTANT: Any deviation from standard UI components should require permission from the user.
The only acceptable classNames are functional layout hints on ScrollArea.Viewport (e.g., p-4 space-y-4) or responsive @container queries. If you find yourself writing custom styles, you are probably missing an existing UI component.
Standard article container pattern:
<Panel.Root role={role}>
<Panel.Toolbar asChild>
<Toolbar.Root>{/* toolbar content */}</Toolbar.Root>
</Panel.Toolbar>
<Panel.Content asChild>
<ScrollArea.Root orientation='vertical'>
<ScrollArea.Viewport classNames='p-4 space-y-4'>{/* Input.Root, Button, etc. */}</ScrollArea.Viewport>
</ScrollArea.Root>
</Panel.Content>
</Panel.Root>
All imports from @dxos/react-ui: Panel, ScrollArea, Input, Button, Clipboard, Toolbar, Card, Icon, useTranslation.
See: plugin-chess/src/containers/ChessArticle/, plugin-discord/src/containers/BotArticle/
src/capabilities/)Plugin modules that contribute functionality to the framework. Each is a single file with a default export using Capability.makeModule(). The barrel index.ts uses only Capability.lazy() exports. Do NOT add non-lazy exports.
See: plugin-chess/src/capabilities/
src/capabilities/layer-specs.ts)Plugins that contribute Effect services to the process-manager runtime do so via Capabilities.LayerSpec entries (see plugin-client/src/capabilities/layer-specs.ts for a minimal reference).
Conventions:
Capability.makeModule(Effect.fnUntraced(...)) activation body. Keep the activation block to just the Capability.contributes(...) list (+ any conditional contributions that depend on runtime config).LayerSpec (ClientLayerSpec, DatabaseLayerSpec, RemoteFunctionExecutionSpec, โฆ). This makes the module-level intent obvious at the callsite.requires, not via outer-scope closures. If a spec needs the Client, require ClientService (or Capability.Service + Capability.get(ClientCapabilities.Client) inside a Layer.unwrapEffect(Effect.gen(...))). If a spec needs contributed capabilities (e.g. operation handlers, blueprint definitions), require Capability.Service and resolve them with Capability.get / Capability.getAll โ this keeps the spec portable and the dependency graph explicit.invariant on missing space context or missing space records. Space-affinity specs that receive a context argument should invariant(context.space, โฆ) and invariant(space, โฆ) on the client lookup โ returning a notAvailable fallback hides configuration bugs in the layer graph.makeModule body. Specs that only apply when a runtime config flag is set (e.g. runtime.client.edgeFeatures.agents) can still read that config from the Client and conditionally append themselves to the contributions list.LayerSpec.LayerContextA spec's affinity determines the slice it lives in and which fields of LayerContext are populated when its factory runs (see packages/core/compute/compute/src/LayerSpec.ts):
| Affinity | Lifetime | LayerContext fields available |
|---|---|---|
application | Process-manager runtime | (none โ {}) |
space | Per space, reused across all processes in space | space |
process | Per spawned process | space, conversation, process (pid) |
conversation and process are process-affinity only โ a space-affinity factory cannot see them. If a service is keyed on conversation (e.g. AiContext.Service, AiSession.Service), it must be process-affinity even though it depends on space-affinity services like Database.Service and Feed.FeedService. The LayerStack initialises lower-affinity slices first, so process specs can require space services without issue.
The LayerContext.conversation field is fed from the spawn environment.conversation, which in turn comes from Operation.invoke(..., { conversation }) or Operation.withInvocationOptions({ conversation }). Operations dispatched by TriggerDispatcher also inherit space/conversation from the parent spawn environment.
LayerSpec.make's factory must return Layer<Provides, never, Requires> โ the error channel is never, so the layer body cannot use typed Effect.fail to signal "this context is invalid". Use Effect.die(new ServiceNotAvailableError(tag.key)) inside the Layer.scoped body when a required LayerContext field is missing:
LayerSpec.make(
{ affinity: 'process', requires: [Database.Service, Feed.FeedService], provides: [AiContext.Service] },
(context) =>
Layer.scoped(
AiContext.Service,
Effect.gen(function* () {
if (!context.conversation) {
return yield* Effect.die(new ServiceNotAvailableError(AiContext.Service.key));
}
const feed = yield* Database.resolve(DXN.parse(context.conversation), Feed.Feed).pipe(Effect.orDie);
const runtime = yield* Effect.runtime<Feed.FeedService>();
const binder = yield* acquireReleaseResource(() => new AiContext.Binder({ feed, runtime }));
return { binder };
}),
),
);
The die surfaces as a defect through LayerStack, and the dispatcher's causeToError extracts the original ServiceNotAvailableError message for logs. Do NOT widen the spec output type with as unknown as casts to return Layer.empty โ that hides the fact that the slice failed to materialise.
LayerStack pruning of unsatisfiable specsA slice contains every spec at its affinity, but the LayerStack prunes specs whose requires aren't satisfied by the parent slice (or by earlier specs in this slice). The slice still initialises with the surviving specs; lookups for tags from dropped specs fail with a precise ServiceNotAvailable at resolve time. This lets a conversation-scoped process spec (like AiContextSpec requiring Database.Service) coexist with process ops that spawn without a space/conversation context.
Practical consequences:
requires โ there is no penalty for an unsatisfied requirement when nobody is asking for what the spec provides.X will report ServiceNotAvailable: X, not the missing transitive dependency. If you need to debug WHY a spec was dropped, check the pruned layer specs with unsatisfied requirements log line emitted by Slice.init (packages/core/compute-runtime/src/LayerStack.ts).See the process slice initialises even when an unrelated process-affinity spec has unsatisfied requirements test in LayerStack.test.ts for the canonical scenario.
Effect.provideService is not enoughProviding a service inline (Effect.provideService(AiContext.Service, โฆ) or Layer.succeed(AiContext.Service, โฆ) via Effect.provide(...)) only applies to the calling fiber. The moment Operation.invoke(child) crosses a process boundary, the child spawn uses its own ServiceResolver/LayerStack and the inline provider is invisible. If any code path can Operation.invoke (or schedule) an op that requires the service, register a production LayerSpec for it โ don't rely on inline providers alone.
src/types/)ECHO type definitions using Effect Schema with Type.makeObject(), LabelAnnotation, and Annotation.IconAnnotation. Use namespace re-exports (e.g., export * as Chess from './Chess'). Include a make() factory function using Obj.make().
See: plugin-chess/src/types/Chess.ts
src/operations/)Operation definitions use Operation.make() with meta, input/output schemas, and services. Handlers use Operation.withHandler() with Effect generators. The barrel exports definitions and a lazy OperationHandlerSet.
See: plugin-chess/src/operations/
The main plugin file wires everything together using Plugin.define(meta).pipe() with AppPlugin helper methods:
| Method | Purpose | Activation Event |
|---|---|---|
addSurfaceModule | React surface components | SetupReactSurface |
addMetadataModule | Type metadata (icon, creation) | SetupMetadata |
addSchemaModule | ECHO type registration | SetupSchema |
addOperationHandlerModule | Operation handlers | SetupOperationHandler |
addTranslationsModule | i18n resources | SetupTranslations |
addBlueprintDefinitionModule | AI blueprints | SetupArtifactDefinition |
addSettingsModule | Plugin settings | SetupSettings |
addAppGraphModule | Graph builder extensions | SetupAppGraph |
addCommandModule | CLI commands | Startup |
addReactContextModule | React context provider | Startup |
addNavigationResolverModule | Navigation resolvers | OperationInvokerReady |
addNavigationHandlerModule | Navigation handlers | OperationInvokerReady |
See: plugin-chess/src/ChessPlugin.tsx
Surfaces are contributed via Capability.contributes(Capabilities.ReactSurface, [...]) with Surface.create().
Common roles: article, section, card--content, object-properties, form-input, dialog.
Common filters: AppSurface.object(AppSurface.Article, Type), AppSurface.object(AppSurface.Card, Type), AppSurface.objectProperties(Type).
See: plugin-chess/src/capabilities/react-surface.tsx
Blueprints provide AI agents with tools and instructions for a domain. Define a blueprint key, gather operations, and use Blueprint.make() with Blueprint.toolDefinitions().
See: plugin-chess/src/blueprints/chess-blueprint.ts
Resources keyed by both typename (for object labels) and meta.id (for plugin-scoped strings). Use useTranslation(meta.id) in components.
See: plugin-chess/src/translations.ts
"private": true.#imports aliases for internal barrels (#capabilities, #components, #containers, #meta, #operations, #types).exports subpaths for anything other plugins need (./types, ./operations).@dxos deps use workspace:*; external deps use catalog:.See: plugin-chess/package.json
Each package.json export subpath needs a matching --entryPoint in the compile task args.
See: plugin-chess/moon.yml
invariant over throwing errors to assert function preconditions.#components, #containers, etc.) instead of deep relative paths.src/components/. The only default exports are in container index.ts files (for React.lazy).import X from '../X';.Panel.Root with role prop in container article/section components.useQuery, useObject, atoms, etc.<input>/<textarea>/<select> or invent color tokens (bg-input, text-primary). Edit objects with Form + schema and use @dxos/react-ui primitives / real @dxos/react-ui-theme tokens. See "Forms, inputs, and theming".moon run plugin-foo:build
moon run plugin-foo:lint -- --fix
moon run plugin-foo:test
moon run plugin-foo:test-storybook
src/components/ and src/containers/ should contain only index files and subdirectories.src/index.ts exports only the plugin and meta. Keep it minimal.types, operations) instead of re-exporting from root.Surface component provides top-level <Suspense> for lazy containers; individual containers only need their own Suspense if they use React.use() or render lazy sub-components.