App capabilities
import * as AppCapabilities from '@dxos/app-toolkit/AppCapabilities';import * as AppCapability from '@dxos/app-toolkit/AppCapability';import * as AppSurface from '@dxos/app-toolkit/AppSurface';import * as UiHooks from '@dxos/react-ui/Hooks';The framework defines how plugins extend each other, but not what an app is made of. @dxos/app-toolkit adds that
for a workspace app such as Composer: a layout, App Graph, settings, translations, the standard surface roles. Its
capabilities are in AppCapabilities, and Composer and any other app built with the toolkit read them. AppCapability has a module
maker for most of them, which sets the activation event and environments so a manifest only names the file.
A tour, by group
Section titled “A tour, by group”Most plugins touch a handful of these. The rest exist for the plugins that build the app’s own features, and a plugin that is not one of those can ignore them.
- Layout.
Layoutis the current layout as an atom: what is open, the sidebar, the mode. Plugins read it; the layout plugin provides it. Changing it goes throughLayoutOperation. - App Graph.
AppGraphBuilderadds nodes and actions,AppGraphis the graph itself for reading, andAppGraphRetentionkeeps nodes loaded while they are off screen. App Graph covers them. - Settings and text.
Settingsadds a section to the settings page, andTranslationsgives the plugin’s strings their text. Settings and state covers settings, and Translations below covers text. - Schema.
Schemaregisters the ECHO types the app can store, query and name. Using ECHO in a plugin covers it. - Agents.
SkillDefinitiongives the assistant a skill,Toolkitadds tools outside a skill, andMcpServerexposes a built-in MCP server. - Files and content.
FileUploaderstores an uploaded file,Exporteradds an export format,TextContentextracts an object’s text for search and agents, andPluginAssetpublishes a file other tools can read, such as the plugin’s spec.
| Capability | Maker | What it adds |
|---|---|---|
AppGraph |
none | The graph itself, for reading. |
AppGraphBuilder |
appGraphBuilder |
Nodes and actions in App Graph. |
AppGraphRetention |
none | Nodes App Graph keeps loaded. |
Exporter |
exporter |
An export format for objects. |
FileUploader |
none | Storage for files people add. |
Layout |
none | The current layout, as an atom. |
McpServer |
mcpServer |
A built-in MCP server. |
PluginAsset |
pluginAsset |
A file other tools can read, such as the plugin’s spec. |
Schema |
schema |
ECHO types the app can store, query and name. |
Settings |
settings |
A section in the settings page. |
SkillDefinition |
skillDefinition |
A skill the assistant can use. |
SpaceTemplate |
spaceTemplates |
A template for new spaces. |
TextContent |
textContent |
The text of an object, for search and agents. |
Toolkit |
none | Tools for the assistant outside a skill. |
Translations |
translations |
Text for the plugin’s strings and type labels. |
AppCapability.surface and AppCapability.operationHandler make modules for the framework’s ReactSurface and
OperationHandler capabilities.
Translations
Section titled “Translations”A plugin never writes its UI text inline. It contributes translation resources, keyed by language and namespace, and reads strings by key, so the app can show them in the person’s language and other plugins can label its objects.
Contribute translation resources
Section titled “Contribute translation resources”A resource maps a language to namespaces, and each namespace to keys and their text. Use the plugin’s key as the
namespace for its own strings, and a type’s typename for the strings that describe the type.
AppCapability.translations contributes the resources; it activates at startup, since labels are needed for the
first screen.
import { import Type
Type } from '@dxos/echo';import type * as import Theme
Theme from '@dxos/react-ui/Theme';
import { const StashNamespace: { key: "com.example.plugin.stash"; make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}
StashNamespace } from './namespace.ts';import { class StashedLink
StashedLink } from './types/StashedLink.ts';
export const const translations: [{ readonly 'en-US': { readonly [x: string]: { readonly 'plugin.name': "Stash"; readonly 'mark-read.label': "Mark as read"; readonly 'typename.label'?: undefined; readonly 'typename.label_zero'?: undefined; readonly 'typename.label_one'?: undefined; readonly 'typename.label_other'?: undefined; readonly 'object-name.placeholder'?: undefined; readonly 'add-object.label'?: undefined; } | { readonly 'typename.label': "Stashed link"; readonly 'typename.label_zero': "Stashed links"; readonly 'typename.label_one': "Stashed link"; readonly 'typename.label_other': "Stashed links"; readonly 'object-name.placeholder': "New link"; readonly 'add-object.label': "Add link"; readonly 'plugin.name'?: undefined; readonly 'mark-read.label'?: undefined; }; readonly "com.example.plugin.stash": { ...; }; };}]
translations = [ { 'en-US': { [const StashNamespace: { key: "com.example.plugin.stash"; make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}
StashNamespace.key: "com.example.plugin.stash"
key]: { 'plugin.name': 'Stash', 'mark-read.label': 'Mark as read', }, [import Type
Type.const getTypename: (input: Type.AnyEntity) => string
getTypename(class StashedLink
StashedLink)]: { 'typename.label': 'Stashed link', 'typename.label_zero': 'Stashed links', 'typename.label_one': 'Stashed link', 'typename.label_other': 'Stashed links', 'object-name.placeholder': 'New link', 'add-object.label': 'Add link', }, }, },] as type const = [{ readonly 'en-US': { readonly [x: string]: { readonly 'plugin.name': "Stash"; readonly 'mark-read.label': "Mark as read"; } | { readonly 'typename.label': "Stashed link"; readonly 'typename.label_zero': "Stashed links"; readonly 'typename.label_one': "Stashed link"; readonly 'typename.label_other': "Stashed links"; readonly 'object-name.placeholder': "New link"; readonly 'add-object.label': "Add link"; }; readonly "com.example.plugin.stash": { readonly 'plugin.name': "Stash"; readonly 'mark-read.label': "Mark as read"; }; };}]
const satisfies import Theme
Theme.export Resource
Resource[];
import * as import AppCapability
AppCapability from '@dxos/app-toolkit/AppCapability';16 collapsed lines
import * as import AppSurface
AppSurface from '@dxos/app-toolkit/AppSurface';import * as import SpaceCapability
SpaceCapability from '@dxos/plugin-space/SpaceCapability';
import { const StashNamespace: { key: "com.example.plugin.stash"; make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}
StashNamespace } from './src/namespace.ts';
const const config: Plugin.PluginConfig<void>
config: import Plugin
Plugin.type PluginConfig<T = void> = { readonly plugin: { readonly key: string; readonly name: string; readonly description?: string | undefined; readonly author?: string | undefined; readonly homePage?: string | undefined; readonly source?: string | undefined; readonly screenshots?: readonly { readonly light?: string | undefined; readonly dark?: string | undefined; }[] | undefined; readonly tags?: readonly string[] | undefined; readonly icon?: { readonly key: string; readonly hue?: string | undefined; } | undefined; readonly spec?: string | undefined; readonly dependsOn?: readonly string[] | undefined; }; readonly publish?: { ...; } | undefined;} & { ...;}
A plugin's dx.config.ts default export: its config data, runtime
Meta
and factory.
PluginConfig = import Plugin
Plugin.const config: <void>({ modules, ...data }: Plugin.ConfigOptions) => Plugin.PluginConfig<void>
Defines a plugin entirely from its dx.config.ts.
config({ plugin: { readonly key: string; readonly name: string; readonly description?: string | undefined; readonly author?: string | undefined; readonly homePage?: string | undefined; readonly source?: string | undefined; readonly screenshots?: readonly { readonly light?: string | undefined; readonly dark?: string | undefined; }[] | undefined; readonly tags?: readonly string[] | undefined; readonly icon?: { readonly key: string; readonly hue?: string | undefined; } | undefined; readonly spec?: string | undefined; readonly dependsOn?: readonly string[] | undefined;}
plugin: { key: string
Reverse-domain NSID — the plugin's globally-unique key (e.g. org.dxos.plugin.excalidraw).
key: const StashNamespace: { key: "com.example.plugin.stash"; make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}
StashNamespace.key: "com.example.plugin.stash"
key, name: string
name: 'Stash', author?: string | undefined
Author or organization name. Only used for bundled plugins. For plugins published to the
registry this field is ignored — the verified publisher (handle ?? did) is used instead.
author: 'Example', description?: string | undefined
description: 'A reading list: save links with a note, and mark them as read.', icon?: { readonly key: string; readonly hue?: string | undefined;} | undefined
icon: { key: string
key: 'ph--tray-arrow-down--regular', hue?: string | undefined
hue: 'indigo' }, tags?: readonly string[] | undefined
tags: ['labs'], }, modules: readonly (Module<any> | undefined)[]
Module specs only; each body is a dynamic import, so reading the config loads no plugin code.
modules: [ import AppCapability
AppCapability.const translations: (resources: Resource | Resource[] | (() => Promise<{ translations: Resource | Resource[];}>), options?: { name?: string; environments?: readonly Environment[];}) => Module<void>
Module contributing translations.
translations( () => import('./src/translations.ts', { ImportCallOptions.with?: ImportAttributes | undefined
with: { type: string
type: 'module-url' } }), ),
The keys the app reads for a type
Section titled “The keys the app reads for a type”The app labels objects of a type from that type’s namespace, so a type with these keys reads well in every menu:
| Key | Where it shows |
|---|---|
typename.label |
The type’s name, such as in the create menu. |
typename.label_zero, _one, _other |
The plural forms, such as a section heading. |
object-name.placeholder |
The name of a new object before the person names it. |
add-object.label |
The create menu’s entry for the type. |
Read a string from a component
Section titled “Read a string from a component”useTranslation takes a namespace and returns t. A key with a count picks its plural form.
import * as import UiHooks
UiHooks from '@dxos/react-ui/Hooks';import { const StashNamespace: { key: "com.example.plugin.stash"; make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}
StashNamespace } from './src/namespace.ts';
export const const PluginTitle: () => React.JSX.Element
PluginTitle = () => { // Strings resolve in the plugin's namespace; the app picks the language. const { const t: TFunction<Namespace<string>, KeyPrefix<Namespace<string>>>
t } = import UiHooks
UiHooks.function useTranslation(ns?: string | string[] | readonly string[] | undefined, options?: UseTranslationOptions<KeyPrefix<Namespace<string>>> | undefined): { toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n | undefined; push(...items: (boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n)[]): number; concat(...items: ConcatArray<boolean | TFunction<...> | import("i18next").i18n>[]): (boolean | TFunction<...> | import("i18next").i18n)[]; concat(...items: (boolean | ConcatArray<boolean | TFunction<...> | import("i18next").i18n> | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[]; join(separator?: string): string; reverse(): (boolean | TFunction<...> | import("i18next").i18n)[]; shift(): boolean | TFunction<...> | import("i18next").i18n | undefined; slice(start?: number, end?: number): (boolean | TFunction<...> | import("i18next").i18n)[]; sort(compareFn?: ((a: boolean | TFunction<...> | import("i18next").i18n, b: boolean | TFunction<...> | import("i18next").i18n) => number) | undefined): UseTranslationResponse<...>; splice(start: number, deleteCount?: number): (boolean | TFunction<...> | import("i18next").i18n)[]; splice(start: number, deleteCount: number, ...items: (boolean | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[]; unshift(...items: (boolean | TFunction<...> | import("i18next").i18n)[]): number; indexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number; lastIndexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number; every<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => void, thisArg?: any): void; map<U>(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, thisArg?: any): U[]; filter<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): (boolean | TFunction<...> | import("i18next").i18n)[]; reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n, initialValue: boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduce<U>(callbackfn: (previousValue: U, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | ...export useTranslation
useTranslation(const StashNamespace: { key: "com.example.plugin.stash"; make: <T>() => <const N extends string>(name: N) => MultiTag<T, `com.example.plugin.stash.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Tag<T, `com.example.plugin.stash.capability.${N}`>;}
StashNamespace.key: "com.example.plugin.stash"
key); return <JSX.IntrinsicElements.h2: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>
h2>{const t: TFunction<"plugin.name", ReactI18NextChild | Iterable<ReactI18NextChild>, StringMap>(key: "plugin.name" | "plugin.name"[], options?: string | TOptions<StringMap> | undefined) => ReactI18NextChild | Iterable<ReactI18NextChild> (+1 overload)
t('plugin.name')}</JSX.IntrinsicElements.h2: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>
h2>;};
import * as import UiHooks
UiHooks from '@dxos/react-ui/Hooks';
export const const LinkCount: ({ count }: { count: number;}) => React.JSX.Element
LinkCount = ({ count: number
count }: { count: number
count: number }) => { const { const t: TFunction<Namespace<string>, KeyPrefix<Namespace<string>>>
t } = import UiHooks
UiHooks.function useTranslation(ns?: string | string[] | readonly string[] | undefined, options?: UseTranslationOptions<KeyPrefix<Namespace<string>>> | undefined): { toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: Intl.NumberFormatOptions & Intl.DateTimeFormatOptions): string; pop(): boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n | undefined; push(...items: (boolean | TFunction<Namespace<string>, KeyPrefix<Namespace<string>>> | import("i18next").i18n)[]): number; concat(...items: ConcatArray<boolean | TFunction<...> | import("i18next").i18n>[]): (boolean | TFunction<...> | import("i18next").i18n)[]; concat(...items: (boolean | ConcatArray<boolean | TFunction<...> | import("i18next").i18n> | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[]; join(separator?: string): string; reverse(): (boolean | TFunction<...> | import("i18next").i18n)[]; shift(): boolean | TFunction<...> | import("i18next").i18n | undefined; slice(start?: number, end?: number): (boolean | TFunction<...> | import("i18next").i18n)[]; sort(compareFn?: ((a: boolean | TFunction<...> | import("i18next").i18n, b: boolean | TFunction<...> | import("i18next").i18n) => number) | undefined): UseTranslationResponse<...>; splice(start: number, deleteCount?: number): (boolean | TFunction<...> | import("i18next").i18n)[]; splice(start: number, deleteCount: number, ...items: (boolean | TFunction<...> | import("i18next").i18n)[]): (boolean | TFunction<...> | import("i18next").i18n)[]; unshift(...items: (boolean | TFunction<...> | import("i18next").i18n)[]): number; indexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number; lastIndexOf(searchElement: boolean | TFunction<...> | import("i18next").i18n, fromIndex?: number): number; every<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean; some(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => void, thisArg?: any): void; map<U>(callbackfn: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, thisArg?: any): U[]; filter<S extends boolean | TFunction<...> | import("i18next").i18n>(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: boolean | TFunction<...> | import("i18next").i18n, index: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => unknown, thisArg?: any): (boolean | TFunction<...> | import("i18next").i18n)[]; reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduce(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n, initialValue: boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduce<U>(callbackfn: (previousValue: U, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | import("i18next").i18n)[]) => boolean | TFunction<...> | import("i18next").i18n): boolean | TFunction<...> | import("i18next").i18n; reduceRight(callbackfn: (previousValue: boolean | TFunction<...> | import("i18next").i18n, currentValue: boolean | TFunction<...> | import("i18next").i18n, currentIndex: number, array: (boolean | TFunction<...> | ...export useTranslation
useTranslation('com.example.type.stashedLink'); // `count` picks `typename.label_zero`, `_one` or `_other`. return <JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span>{`${count: number
count} ${const t: TFunction<"typename.label", string, { count: number;}>(key: "typename.label" | "typename.label"[], options?: string | TOptions<{ count: number;}> | undefined) => string (+1 overload)
t('typename.label', { count: number
Count value used for plurals
count })}`}</JSX.IntrinsicElements.span: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span>;};
App Graph nodes and actions take a label as a key and namespace, ['mark-read.label', { ns }], and the app
translates it where it renders.
Surface roles
Section titled “Surface roles”The roles a plugin fills most often are in AppSurface from @dxos/app-toolkit:
| Role | Where it renders | What it is given |
|---|---|---|
Article |
The main content area, one object at a time | subject, attendableId |
Section |
An object inside a stack of objects | subject, attendableId |
CardContent |
The body of a card, in search results and boards | subject |
ObjectProperties |
The object’s properties panel | subject |
Dialog, Popover |
A dialog or popover a plugin opens by name | component, props |
Navigation |
The navigation sidebar | current, the open item’s id |
StatusIndicator |
The status bar | no data |
A plugin can render into any role another plugin defines, and declare its own. See Surfaces.
Contribute settings, metadata and schema registrations
Section titled “Contribute settings, metadata and schema registrations”A settings contribution names a prefix, a schema and a writable atom. The app renders a form from the schema and writes the person’s changes to the atom.
import * as import Effect
Effect from 'effect/Effect';import * as import Atom
Atom from 'effect/reactivity/Atom';import * as import Schema
Schema from 'effect/Schema';import * as import Struct
Struct from 'effect/Struct';import * as import Capability
Capability from '@dxos/app-framework/Capability';import * as import AppCapabilities
AppCapabilities from '@dxos/app-toolkit/AppCapabilities';
export const const Settings: Schema.Struct<{ readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;}>
Settings = import Schema
Schema.function Struct<{ readonly showSeconds: Schema.optional<Schema.Boolean>;}>(fields: { readonly showSeconds: Schema.optional<Schema.Boolean>;}): Schema.Struct<{ readonly showSeconds: Schema.optional<Schema.Boolean>;}>
Defines a struct schema from a map of field schemas.
Details
Each field value is a schema. Use
optionalKey
or
optional
to
mark fields as optional, and
mutableKey
to mark them as mutable.
The resulting schema's Type is a readonly object type with the fields'
decoded types. The Encoded form mirrors the field schemas' encoded types.
Declared fields may be inherited and are copied to own properties in the
output. The __proto__ field is accepted only when it is an own property.
Parsing does not guarantee that output keys retain their input order.
Example (Defining a basic struct)
import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.String, age: Schema.Number, email: Schema.optionalKey(Schema.String)})
// { readonly name: string; readonly age: number; readonly email?: string }type Person = typeof Person.Type
Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) // => { name: "Alice", age: 30 }
Struct({ showSeconds: Schema.optional<Schema.Boolean>
showSeconds: import Schema
Schema.const optional: optionalLambda<Schema.Boolean>(self: Schema.Boolean) => Schema.optional<Schema.Boolean>
Type-level representation returned by
optional
.
Marks a struct field as optional, allowing the key to be absent or
undefined.
Details
The resulting property may be absent or explicitly set to undefined.
Equivalent to optionalKey(UndefinedOr(S)).
Use
optionalKey
instead if you want exact optional semantics (absent
only, not undefined).
Example (Defining an optional field accepting undefined)
import { Schema } from "effect"
const schema = Schema.Struct({ name: Schema.String, age: Schema.optional(Schema.Number)})
// { readonly name: string; readonly age?: number | undefined }type Person = typeof schema.Type
optional( import Schema
Schema.const Boolean: Schema.Boolean
Type-level representation of
Boolean
.
Schema for boolean values. Validates that the input is typeof "boolean".
When to use
Use to validate values that are already JavaScript booleans.
Boolean.BottomWithoutNew<boolean, boolean, never, never, Boolean, Boolean, boolean, boolean, readonly [], boolean, "readonly", "required", "no-default", "readonly", "required">.annotate(annotations: Schema.Annotations.Bottom<boolean, readonly []>): Schema.Boolean
annotate({ Annotations.Augment.title?: string | undefined
title: 'Show seconds' }), ),}).Struct<{ readonly showSeconds: optional<Boolean>; }>.mapFields<{ readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;}>(f: (fields: { readonly showSeconds: Schema.optional<Schema.Boolean>;}) => { readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;}, options?: { readonly unsafePreserveChecks?: boolean | undefined;} | undefined): Schema.Struct<{ readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;}>
Returns a new struct with the fields modified by the provided function.
Details
Options:
-
unsafePreserveChecks - if true, keep any .check(...) constraints
that were attached to the original union. Defaults to false.
Warning: This is an unsafe operation. Since mapFields
transformations change the schema type, the original refinement functions
may no longer be valid or safe to apply to the transformed schema. Only
use this option if you have verified that your refinements remain correct
after the transformation.
mapFields(import Struct
Struct.const map: <mutableKeyLambda>(lambda: mutableKeyLambda) => <S>(self: S) => { [K in keyof S]: S[K] extends Schema.Constraint ? Schema.mutableKey<S[K]> : never; } (+1 overload)
Applies a
Lambda
transformation to every value in a struct.
When to use
Use when you want to apply the same function to every value in a struct.
Details
The lambda must be created with
lambda
so the compiler can track the
output types.
Example (Wrapping every value in an array)
import { pipe, Struct } from "effect"
interface AsArray extends Struct.Lambda { <A>(self: A): Array<A> readonly "~lambda.out": Array<this["~lambda.in"]>}
const asArray = Struct.lambda<AsArray>((a) => [a])const result = pipe({ width: 10, height: 20 }, Struct.map(asArray))result // => { width: [10], height: [20] }
map(import Schema
Schema.const mutableKey: mutableKeyLambda
Type-level representation returned by
mutableKey
.
Makes a struct field mutable (removes the readonly modifier on the property).
Use
readonlyKey
to reverse.
mutableKey));
export interface interface Settings
Settings extends import Schema
Schema.namespace Schema
Namespace of type-level helpers for
Schema
.
A typed view of a schema that tracks only the decoded (output) type T.
Details
Use Schema<T> as a constraint when you want to accept "any schema that
decodes to T" and do not need to know or constrain the encoded
representation, required services, or any other type parameters.
This is a structural interface — concrete schema values are produced by the
constructors in this module (e.g.
Struct
,
String
,
Number
).
When you also need the encoded type or service requirements, use
Codec
.
Example (Accepting any schema decoding to string)
import { Schema } from "effect"
const accept = (_schema: Schema.Schema<string>): void => {}
accept(Schema.String)accept(Schema.NonEmptyString)
Schema.type Schema<out T>.Type<S> = S extends { readonly Type: infer T;} ? T : never
Extracts the decoded Type from a schema.
Example (Extracting the decoded type)
import { Schema } from "effect"
const Person = Schema.Struct({ name: Schema.String, age: Schema.Number })type Person = Schema.Schema.Type<typeof Person>// { readonly name: string; readonly age: number }
Type<typeof const Settings: Schema.Struct<{ readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;}>
Settings> {}
export const const SettingsModule: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>
SettingsModule = import Capability
Capability.const makeModule: <void, Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, never>(fn: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>) => (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>
Helper to define a capability module with explicit typing.
Wraps the default export function to provide better type inference and make the pattern explicit.
This helper provides explicit typing for the module activation function,
making it clear that the function should:
- Access declared
requires via yield* (or the ambient Capability.Service/Plugin.Service)
- Return an array of typed
Contribution
s (see
contribute
/
contributeAll
)
makeModule(() => import Effect
Effect.const sync: <Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>>(thunk: LazyArg<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>>) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, never>
Creates an Effect that represents a synchronous side-effectful computation.
When to use
Use when you need to wrap a synchronous side-effectful operation that is not
expected to throw.
Details
The provided function is evaluated lazily when the effect runs.
Gotchas
The function must not throw. If it throws, the thrown value is treated as a
defect, not as a typed failure. Use try when throwing is expected.
Example (Capturing synchronous logging in an Effect)
import { Effect } from "effect"const output: Array<unknown> = []
const log = (message: string) => Effect.sync(() => { void output.push(message) // side effect })
// ┌─── Effect<void, never, never>// ▼const program = log("Hello, World!")Effect.runSync(program)output // => ["Hello, World!"]
sync(() => { // A writable atom. Real plugins persist it with `KvsStore.make` from // `@dxos/effect`. const const atom: Atom.Writable<Settings, Settings>
atom = import Atom
Atom.const make: <Settings>(initialValue: Settings) => Atom.Writable<Settings, Settings> (+5 overloads)
Creates an atom from a synchronous value or read function, or from an Effect or Stream whose state is exposed as an AsyncResult; plain values create writable state atoms.
make<interface Settings
Settings>({ showSeconds?: boolean | undefined
showSeconds: false }); return import Capability
Capability.const contribute: <Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">>(capability: Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">, implementation: AppCapabilities.Settings) => Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">> (+1 overload)
contribute(import AppCapabilities
AppCapabilities.const Settings: Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">
Settings, { prefix: string
prefix: const ClockNamespace: { key: "com.example.plugin.clock"; make: <T>() => <const N extends string>(name: N) => Capability.MultiTag<T, `com.example.plugin.clock.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Capability.Tag<T, `com.example.plugin.clock.capability.${N}`>;}
ClockNamespace.key: "com.example.plugin.clock"
key, schema: Schema.Codec<any, any, never, never>
schema: const Settings: Schema.Struct<{ readonly showSeconds: Schema.mutableKey<Schema.optional<Schema.Boolean>>;}>
Settings, atom: Atom.Writable<any, any>
atom, }); }),);
In a manifest, each maker takes the loader of the module that contributes the value:
import * as import AppCapability
AppCapability from '@dxos/app-toolkit/AppCapability';
// Each maker declares the capability and its default activation event, so a// manifest names only the loader.export const const modules: Capability.Module<void>[]
modules = [ import AppCapability
AppCapability.const settings: <void, void, readonly [], readonly []>(loader: Capability.LoadModule<void, readonly [], readonly [Capability.MultiTag<AppCapabilities.Settings, "org.dxos.app-framework.capability.settings">]>, options?: Capability.MakerOptions<readonly [], readonly [], void, void> | undefined) => Capability.Module<void>
Module maker contributing settings. Ungated by default because settings VALUES are read
app-wide through the strict useAtomCapability hook — including from components that mount
with the shell at ready (the transcription driver's ReactContext, DeckLayout) and from
requires on boot modules — where a post-ready gate trips the missing-capability invariant.
A plugin whose settings are read only from its own deferred surfaces declares
activatesOn: ActivationEvents.Idle to keep them off the startup pass.
Stated explicitly rather than inherited: omitting activatesOn now normalizes to Idle, which
is precisely the post-ready gate this paragraph rules out.
settings(async () => ({ default: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">> | readonly Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>[], Error, Capability.Requirements<readonly []>>
default: const SettingsModule: (props: void) => Effect.Effect<Capability.Contribution<Capability.CapabilityIdentifier<"org.dxos.app-framework.capability.settings", "multi">>, never, Scope>
SettingsModule })), import AppCapability
AppCapability.const translations: (resources: Resource | Resource[] | (() => Promise<{ translations: Resource | Resource[];}>), options?: { name?: string; environments?: readonly Capability.Environment[];}) => Capability.Module<void>
Module contributing translations.
translations([ { 'en-US': { [const ClockNamespace: { key: "com.example.plugin.clock"; make: <T>() => <const N extends string>(name: N) => Capability.MultiTag<T, `com.example.plugin.clock.capability.${N}`>; makeSingleton: <T>() => <const N extends string>(name: N) => Capability.Tag<T, `com.example.plugin.clock.capability.${N}`>;}
ClockNamespace.key: "com.example.plugin.clock"
key]: { 'plugin.name': 'Clock' } } }, ]),];
The tutorial plugin registers its type with AppCapability.schema, whose module’s default export is the list of types:
import { class StashedLink
StashedLink } from '../types/StashedLink.ts';
export default [class StashedLink
StashedLink];
App capability or a new one
Section titled “App capability or a new one”Use an app capability when you are adding to something the app already shows: a setting, a node, a skill. Declare a
new capability when your plugin is the host and other plugins add to it, the way the Space plugin declares
SpaceCapabilities.CreateObjectEntry for the types in its create menu. Capability
shows how.
Related
Section titled “Related”| Page | What it covers |
|---|---|
| Capability | Declaring and reading capabilities. |
| Plugin | Modules and makers. |
| React | The other hooks a plugin component uses. |