# LoomWeaver > LoomWeaver is a domain-agnostic plugin & UI platform — the *loom* on which products ("weavers") weave. Products run as plugin bundles on top of it; the platform itself contains **zero domain logic** (model: VS Code / Backstage / Eclipse Theia — a thin core that can only do plugins, the "actual app" is a first-party plugin bundle). A plugin ("weaver") consumes one uniform `ctx` contract from `@loom/plugin-sdk`; a capability broker (**default-deny**) governs what each plugin may do. A product is a thin **distribution** that composes `@loom/shell` + weavers from the published packages. Frontend: Angular + Nx. The platform ships **no server** — the backend seam (settings, auth, per-tenant secrets, egress) is the product's own backend, which implements the frontend ports (settings store / working-state store / `AuthSource`); the UI runs standalone with local/anonymous defaults. System-near local power lives in **Treadle** (a user-installed companion agent, speaks MCP). Status: the platform is **built and published** as six versioned npm packages: `@loom/plugin-sdk`, `@loom/shell`, `@loom/sandbox-kit` (UI assets for sandboxed plugins) and the scaffolding trio `@loom/cli`, `@loom/devkit` (Nx generators) and `@loom/mcp` (an MCP server for AI assistants). This is a curated map for people — and their AI assistants — building on or integrating LoomWeaver. For a single-fetch brief with the complete contract inlined, see [`llms-full.txt`](llms-full.txt). Paths are repo-relative. ## Start here - [Architecture](docs/architecture.md): the mental model — platform / weaver / distribution, the uniform `ctx`, default-deny capabilities, the three RPC boundaries. Read this first. - [Getting started](docs/getting-started.md): scaffold a running, branded product with a plugin already contributing to it, in ~5 minutes, using `@loom/cli`. - [Manual setup](docs/manual-setup.md): the same app wired by hand (~15 min) — the shell renders INSIDE the generated `app-root`, so nothing of `ng new`/Nx is deleted and every provider has one address (`src/app/app.config.ts`). Also the Nx differences (`project.json`, `@source` depth, `@loom/devkit`), SSR (`RenderMode.Client`), and why Module Federation is not the plugin transport. - [Samples](docs/samples.md): complete copyable recipes — a sidebar view with persisted `VIEW_STATE`, a routable surface with a `:param`, one command behind a shortcut + bar button + menu entry, a settings section, access gating, a confirm/progress/toast flow, and the full `VIEW_STATE` treatment (filter, active sub-tab, expanded nodes, scroll position in one shape) next to the counter-example that loses all of it. Each compiles against the published contract. ## Build - [Authoring a weaver](docs/authoring-a-weaver.md): build UI — **surfaces** (`ctx.registerSurface`, the one contract; every contributed pane is an Obsidian-style tab group the user splits/moves freely, `VIEW_STATE` travels with a moved tab; **the presentation form — component · deferred component · iframe · container — is valid at every mount point**, so a non-routable surface may be an iframe docked in a sidebar, and a container child may be one too. A container declares **how it opens** (`ContainerSpec.initial` takes an area tree, not just a list), and a child may carry a **relative address** (`{ surface, segment }`) so the browser address names the focused child — deep links, back/forward and reload work inside a container, a popped-out one freezes its address, and `CONTAINER_HANDLE.open(path, label?)` lets a child open a sibling at a concrete address. A docked surface has no address: its channel's `navigate` is a no-op with a dev warning, and the host pushes `instanceId` and `params` so it knows which mount and which container it is in), **retention & unsaved work** (a hidden surface is destroyed as soon as it is clean — `VIEW_STATE` is the survival surface, `retain: 'always'|'never'` keeps a live instance — host-mounted in every pane and keyed to the pane, so a split shows two independent instances and a role handoff never swaps them; the price is a fabricated route: no resolvers/query params, never combine `retain` with `subRoutes` —, editors implement `DirtySurface` — `surfaceDirty`/`surfaceSave`/`surfaceBeforeClose` — for the host's Save·Discard·Cancel close dialog and `beforeunload`; `saveOn: 'hide'` auto-saves; a sandboxed surface pushes `setDirty` and may expose `beforeClose` on its channel), the URL-addressed **content area** (routes and tabs; one strip per pane showing everything it holds, visiting a route opens its tab, exactly one pane carries the address and that role follows the click — navigating to a surface another pane already holds reaches it there instead of duplicating it — and `routable: { chromeless: true }` marks the full-area screen that never becomes a tab and shows no strip; `closable: false` makes an overview screen permanent — it applies to **every** tab of that surface, so it fits a parameter-free route and almost never `doc/:id`; `padded: false` hands the pane edges to a surface that **is** the content — a viewer, a canvas, a map — and travels with it into splits, sidebars and pop-outs), commands, context menus (`ctx.registerMenuItem` + item `menu?` slots on host chrome, or `ctx.ui.openMenu` on your own view body; only the element that opens a menu suppresses the browser's own, so a text field in your view keeps cut/copy/paste/spellcheck), **auth-aware access gating** (`access` on contributions + `ctx.session`), dialogs (`ctx.ui`), host facts (`ctx.host`), the focused content surface (`ctx.activeContent`), settings, custom icons (`ctx.contributeIcons`), custom theme (`ctx.contributeTheme(tokens, dark?)` — re-skin the whole app: colors + UI font, with optional dark-mode overrides; a producer theme is live per-window state — persist + opt into sync to mirror it across windows), i18n. Your view also works unchanged in a **pop-out window** — if it draws sub-tabs, switch them locally when host-mounted (`routeConfig === null`). `ctx` is the supported surface; a weaver's own `customElements.define` is an explicitly unsupported escape hatch (trusted only, cannot be undone, breaks on a move to the sandbox the UI ladder’s last rung). With complete examples. - [Building a distribution](docs/building-a-distribution.md): compose weavers into a branded product — layout/regions, content-area routing (`provideShellRouter`), a built **command-palette entry** (`provideCommandPaletteEntry` — a top-bar search badge with the OS-correct shortcut, and `formatChord` to show one anywhere; the palette has two entry points — `mod+k` for commands and `mod+p` **Quick-Open** for open tabs across all split panes plus every parameter-free, non-chromeless registered route), **panes & splits** (always on; every user-facing gesture switchable via `provideShellFeatures({ content, sidebar, rail, workspaces, windows, commands })` — one switch takes the affordance **and** the gesture (`content: { splitRight: false }` also removes the drop edges and `mod+\`), everything is on by default; a *contribution* is removed with `provideShell({ omit })` instead, and where a capability is both, the feature wins and takes the menu entry with it and the retention default via `provideShell({ retention })` — destroy-on-hide with the host's unsaved-changes dialog guarding every close, plugin disable/uninstall/update and workspace reset; self-remembering named workspaces built in — always exactly one active, switching restores each workspace's own arrangement without asking, reset returns to its baseline; a distribution ships ready-made ones with `provideWorkspaces` — a `WorkspaceDefinition` declares the sidebar views and a nested `tabs`/`rows`/`columns` content area, and a rail item carrying `workspace: ` puts one a click away; the user curates rail and sidebars alike, an entry or view living in exactly one rail/sidebar; a **user-saved** workspace has no icon to declare, so the shell derives a two-letter badge from its name — first-and-last letter of one word, initials of two — and only a colliding newcomer steps aside, so no existing badge is renamed), persistence (**two ports of one `KeyValueStore` shape**: `provideSettingsStore` for genuine settings — the REST-worthy port — and `provideWorkingStateStore` for working state, device-local by default; **identity-scoped per-user state** on shared browsers via `provideIdentityScopedStores` — boot-latched, wraps both ports — paired with `provideAuthSource(..., { onIdentityChange: 'reload' })`, plus the authoritative storage-key inventory with port/scope/sync axes), **cross-tab live sync & pop-out windows** (`StateSyncService` mirrors synced state across same-origin windows live, and `notifyRemoteChange` opens the same apply path to a backend push transport; `PopoutService` opens any surface in its own `/popout/…` browser window — nothing to wire; a pop-out offers only what belongs beside a single surface, so Quick-Open is absent, content navigation is refused, and commands are main-window-only by default until they declare `popout: true`), capability grants, **auth integration** (`provideAuthSource` feeds the session · product-owned login page/dialog · `provideUnauthorizedRedirect`), branding, **distribution icons** (`provideIcons` — naming a shipped icon **replaces** it app-wide and the replacement travels into sandboxed surfaces; a weaver's `ctx.contributeIcons` stays first-wins so an installed plugin cannot repaint the chrome), i18n composition (namespaces **add** strings under `name.*` and can never collide; `provideTranslationOverrides()` + `/i18n/overrides/.json` **rewords the shell**, merged key by key and applied last, so you name only what you change and inherit every key a later release adds — and it takes an optional directory, so one build can carry several wordings and pick one while composing), PWA, **sandboxed (non-Angular) plugins** over iframe + RPC (`provideSandboxPlugins`, experimental first rung) with the **sandbox UI kit** served under `/sandbox-kit/` (`@loom/sandbox-kit` — the `` element family, compiled `.lw-*` class contracts and Penpal as distribution-served assets; full `--lw-*` token push), and a **plugin store** (curated same-origin catalog, runtime install with a capability-consent dialog, and **updates driven by the catalog `version`** — a newer entry offers an update, respawns the plugin live and re-asks consent only if the declaration grew; `providePluginCatalog`). The page opens with the whole provider surface **indexed by intent** ("I want to …" → which provider), and a dev-only **composition report** (`loomweaver.report()` in the console) that prints what this product composed and warns about what lands nowhere — an `omit` that matched nothing (naming the prefix you probably meant), a settings button or menu entry pointing at a command no one registers, and, already at startup, a bar/rail/view contribution aimed at a region the layout does not declare. - [The plugin system](docs/plugins.md): the distribution's view of plugins — the three rungs (trusted composed, sandboxed iframe over RPC, community-installed at runtime) against one contract, which parts of `ctx` cross the sandbox boundary and which stay trusted-only (a sandboxed surface may now be **docked** or a **container**, not only a content tab; `access` stays rejected), default-deny capabilities, what the user may revoke/disable/uninstall, the activation and respawn lifecycle, and the contribution-id collision trade. - [Backend integration](docs/backend-integration.md): the product hand-off — the platform ships no server; wire your backend behind **four seams**: the settings store (with the all-or-nothing `peek` rule that separates a local from a network store), the optional working-state store, `AuthSource`, and **translations** (swap Transloco's loader to serve bundles from your API; the initial language is read from `localStorage` before DI, and a language beyond the shipped English/German is taken over wholesale). ## The contract — public API - [`@loom/plugin-sdk` public API](platform/libs/core/plugin-sdk/src/index.ts): everything a plugin imports (nothing else is public API). - [Plugin contract](platform/libs/core/plugin-sdk/src/lib/plugin.ts): `Plugin`, `PluginManifest`, `PluginContext` (the `ctx` a plugin holds), `PluginUi` (`ctx.ui`), `PluginHost` (`ctx.host`), `PluginSession` (`ctx.session`). - [Capabilities](platform/libs/core/plugin-sdk/src/lib/capability.ts): the coarse capabilities a plugin *declares* and a distribution *grants* — default-deny (`contributions`/`ui`/`host`/`navigation`/`session`/`theme`). - [Access gating](platform/libs/core/plugin-sdk/src/lib/auth.ts): `AuthSnapshot`, `AccessRequirement`, and the pure predicates — how a contribution reacts to login state + roles. Distinct from capabilities: what a *user* may see vs. what a *plugin* may do. - [Worked example — the testbed weaver](platform/libs/weavers/testbed-weaver/src/lib/plugin/testbed.plugin.ts): a real in-repo plugin (dogfood). All domain content lives in a weaver like this, never in the core. - [`loom-testbed` composition root](platform/apps/loom-testbed/src/main.ts): a worked distribution — `provideShellRouter` / `provideLayout` / `providePlugins` / `provideCapabilityGrants` / branding. ## Scaffolding & tooling (AI-assisted setup) Deterministic scaffolding + validation for building on LoomWeaver. Prefer these over hand-wiring. - [Scaffolding](docs/scaffolding.md): the guide — which of the three adapters applies to you, what the tools return, and every weaver option. - [`@loom/devkit`](platform/libs/tooling/devkit/README.md): the **Nx generator collection** — install as a dev dependency (`npm i -D @loom/devkit`). The fullest adapter: Nx hands it a virtual tree, so it is the only one that also *changes* files (registers the project, adds the tsconfig alias). Placement is read from the workspace, not assumed — `--directory`, `--import-path` (defaults to your root manifest's npm scope), `--app` (inferred when there is exactly one application, an error naming the candidates when there are several), `--tags`, `--prefix`, `--unit-test-runner`. Generators — `weaver` (feature-composable: `--command`/`--shortcut`/`--menu`/`--bar-item`/`--settings`/`--about`/`--instanceable` [docks the surface — named instances exist only for a non-routable one]/`--container` [ nested pane tree at `/:id`; mutually exclusive with `--instanceable`]/`--access`), `sandbox-plugin` (framework-agnostic iframe), `distribution` (runnable app; `--styles precompiled` emits a one-line stylesheet importing our pre-compiled `shell.css`, so the app needs **no Tailwind** — default `tailwind` compiles the source theme), `auth-source` + `settings-store` (backend ports), `theme` (`--preset bootstrap` maps all 29 `--lw-*` tokens onto Bootstrap 5.3's `--bs-*`), `layout`. E.g. `nx g @loom/devkit:weaver --id=notes --settings --about`. Generated test wiring is `@nx/angular:unit-test` (Vitest); a weaver library compiles its specs with the build options of the app that `--app` resolves, and its i18n bundle is served through that app via a generated assets glob. Plus pure cores: `generate(recipe, input) → FileMap` and `validateManifest`/`validateI18nParity`. - [`@loom/cli`](platform/libs/tooling/cli/README.md): the scaffolding **CLI** — the primary path for a product repo, because it needs no Nx workspace, no LoomWeaver checkout and no assistant: `npx @loom/cli weaver --id notes --command --shortcut 'mod+shift+n' --out src/lib/notes`. Same generators as MCP over the same pure core, so output is identical; it is the adapter that writes the files itself. `--dry-run`, `--force` (an existing file otherwise stops the run), `--strict` (validation warnings fail the exit code, for CI); as the writing adapter it refuses paths escaping `--out`, replaces symlinks rather than following them, and fails on an unknown flag. Also `validate-manifest`, `validate-i18n --dir ` and `validate-catalog --file ` (the plugin store catalog is parsed defensively by the host — unknown fields, malformed values and whole entries disappear silently, so the validator names each consequence). - [`@loom/mcp`](platform/libs/tooling/mcp/README.md): an MCP server exposing the same scaffolding + validation as tools (`scaffold_*` return a file map; `validate_*` return findings) for AI assistants — transport-neutral core, stdio now. Published as a self-contained bundle, so a product repo that already consumes `@loom/*` registers it in `.mcp.json` with `{ "command": "npx", "args": ["-y", "@loom/mcp"] }` and gets scaffolding without a local devkit checkout (see its README). ## Reference - [Shell anatomy](docs/reference/shell-anatomy.md): the region vocabulary (rail / panel / bar / content) and docks a distribution declares. - [Host services](docs/reference/host-services.md): every runtime service a distribution may inject — `DialogService`, `NotificationService`, `SettingsService`, `CommandService`, `AuthContext`, `ContentTabsService`, `UpdateService`/`VersionService`, `PopoutService`, `StateSyncService`, the plugin-management services — plus contributing chrome without a plugin (`provideViews`/`provideBarItems`/`provideRailItems`). A plugin never injects these; it goes through the brokered `ctx`. - [Access gating](docs/reference/access-gating.md): the complete `access` reference — the `AuthSnapshot`/`AccessRequirement` vocabulary (`claims` is carried but never matched), which surface reacts how to `hide`/`disable`, gated routes and the placeholder twin, the three readers of the session, the `onIdentityChange` policy, and why capability grants and access gating are orthogonal. - [Design tokens & `` vocabulary](docs/reference/design-tokens.md): the semantic tokens and host UI building blocks to use inside plugin templates (never raw palette colors). The vocabulary is **colour and type only** — sizes, radii and spacing have no tokens by decision; a product that must change one writes plain **unlayered** CSS, which beats everything the shell paints because all of it lives in cascade layers. - [Accessibility](docs/reference/accessibility.md): the WCAG 2.1 AA guardrail the host meets and weavers inherit. - [`@loom/shell` — the neutral host chrome](platform/libs/core/shell/README.md): renders regions and holds contributions; domain-pure. ## Packages Six npm packages, one shared version: `@loom/plugin-sdk`, `@loom/shell`, `@loom/mcp` (the AI-scaffolding MCP server), `@loom/cli` (the scaffolding CLI — same generators, driven by flags instead of an assistant), `@loom/devkit` (the Nx generator collection), `@loom/sandbox-kit` (static UI assets — the `` element bundle, compiled `.lw-*` CSS and Penpal — a distribution serves under `/sandbox-kit/` for sandboxed plugins). (No server packages — the platform is frontend-only.) ## For AI assistants - [`llms-full.txt`](llms-full.txt): the complete contract (interfaces + provider signatures) and canonical distribution/weaver code, inlined for single-fetch ingestion. - [Brand assets](assets/brand/): logo and colors (blue `#2E96C9`, gold `#C59A2F`).