compose-atoms
Decompose a heavy domain feature into mountable capability atoms. Use when a Viewer/Page/index.tsx owns fetch, filters, mutations, modals, and host integrations together; a visual split still leaves store calls and actions on the page; a portal, embed, share, mobile, or micro-app needs a subset of the same domain; or a new capability is landing as another `readOnly`/`mode`/`variant` flag. Triggers on `compose-atoms`, sink state, 状态下沉, 重业务拆分, 拆成原子, 原子组件, 组装, god component, fat viewer, module graph, slot composition, host seam.
git clone --depth 1 https://github.com/lobehub/lobehub /tmp/compose-atoms && cp -r /tmp/compose-atoms/.agents/skills/compose-atoms ~/.claude/skills/compose-atomsSKILL.md
# Compose Atoms
A heavy domain is a **kit**, not a viewer with modes. Each host is an import list. An atom owns the data, actions, and dependencies of one capability. The assembler only chooses what to mount.
This is a **module-graph** split. Hiding UI does not drop a module. A host that never imports a file is the only host that does not ship it.
Do not use this skill to slice one component into smaller files. That is `react`.
## When
The folder is a product surface (Conversation, Acceptance, Document, Verify, Task, …) and at least one of these is true:
- One entry owns read + write + workflow + host integration.
- A second host already exists or is the next change (page, portal, share, mobile, popup, micro-app).
- The next feature is another boolean / `mode` / `variant` on the fat tree.
- Reusing a header, list, or card would import trays, stores, or chat.
If the file is large but has **one** capability and **one** host, stop. Use `react`.
## Grain
An atom is the **smallest unit a host is allowed not to mount**.
Ask, for every chunk: _would any host ship the rest and skip this?_ If yes, it is an atom. If no, it stays inside its parent.
That grain is coarser than a visual section and finer than the whole page.
| Too coarse | Right grain | Too fine |
| ---------------------------------- | ----------------------------------- | --------------------------------------------- |
| One `Viewer` with `readOnly` | Identity, goal, list, decision, … | Every badge, row, and icon as a public export |
| `Chat` as a single import | List, composer, intervention, … | Every message sub-row the page then rewires |
| `Document` as editor + every panel | Canvas, header, comments as omitted | Internal toolbar buttons |
A **workflow** (focus review, thread, publish) is one atom to hosts that skip the whole mode. Inside it, keep splitting only if a host would mount a piece of that workflow alone.
## Sink State
The split is worthless if the assembler still holds the domain state. **Imports follow the hook.** A `useStore` / `useX` / `handleAccept` left on the page keeps that module (and everything it imports) on every host that mounts the page.
Sink everything the atom needs into the atom:
| Sinks into the atom | Stays on the assembler |
| ------------------------------------------ | --------------------------------------------- |
| Resource fetch | The record id / scope identity |
| Derived view model (counts, labels, scope) | Which atoms are mounted (the import list) |
| Transient UI (filter, collapse, pending) | Host layout state (focused vs overview, rail) |
| Mutation handlers and their stores/modals | Host seams (provider value the runtime owns) |
```tsx
// wrong — visual split, state still on the page
const Page = () => {
const { data, mutate } = useX(id);
const [filter, setFilter] = useState('all');
const accept = () => void mutate(...);
return (
<>
<Identity data={data} />
<List data={data} filter={filter} onFilter={setFilter} />
<DecisionBar onAccept={accept} />
</>
);
};
// right — each atom reads and acts; page only assembles
const Page = () => (
<Scope id={id}>
<Identity />
<List />
<Decision />
</Scope>
);
```
Do not lift "to fetch once" or "so siblings share data". Two atoms calling the same SWR hook share the cache by key. Sibling writes go through that cache (or a store slice the write atom imports), not through page `useState`.
Lift only when the state **is** the assembler's job: which workflow is on screen, whether a rail is open. If a value is used to render or mutate one capability, it belongs in that capability.
## Kit
| Piece | Owns | Must not own |
| ---------------- | ------------------------------------------------- | -------------------------------------------------- |
| Domain primitive | Types, predicates, formatters | React, stores, services |
| Data access | One resource, one file | Sibling list / infinite / document hooks |
| Read atom | Present + local UI state + its own fetch | Mutations, trays, owner services, host stores |
| Write atom | One mutation and its UI | Page layout, unrelated writes |
| Workflow atom | A mode assembled from other atoms | Becoming the only entry other hosts can import |
| Domain slot | `ReactNode` hole for another **same-domain** atom | A callback whose implementation lives in this file |
| Host seam | Optional host-provided UI, **null by default** | A fallback that imports the host graph |
| Assembler | Layout + the import list | Domain actions; domain fetch except local chrome |
A visual block that both reads and writes is a read atom + a write atom, joined by a domain slot.
Two seam types — do not mix them:
- **Domain slot** — same feature, optional capability (`editSlot`, `toolbar`). The host imports the write atom and passes it in.
- **Host seam** — only some runtimes can resolve it (chat store, topic drawer, Electron). Context default is `null`. The micro-app / share page never imports the provider value. See `split-micro-app` for the runtime cut.
## Ownership
State and imports live in the atom that needs them.
- Read atoms call the isolated resource hook. Filter / collapse / expand stay in the atom.
- Write atoms call the same hook, then mutate. They import their own modals and services. They return `null` when the record is not writable.
- Workflow atoms compose other atoms. They still do not become the lighAdd documentation for a new AI provider — usage docs, env vars, Docker config, image resources.
Add server-side environment variables that control default values for user settings.
Agent runtime lifecycle hooks. Use for before/after tool or step hooks, tool mocks, human intervention, sub-agent calls, context compression, evals, callAgent, or lifecycle events.
Build or extend LobeHub Agent Signal pipelines. Use for signal sources, signal/action types, policies, middleware, workflow handoff, dedupe, scope behavior, or observability.
Agent tracing CLI for execution snapshots. Use for agent-tracing, traces, snapshots, LLM call inspection, context engine data, agent step analysis, execution debugging, or pulling remote/production traces ("拉线上 tracing") by operation id. Also the first stop for debugging agent tool calls — wrong or missing tool_calls, unexpected tool arguments or results, which tools were available at a step, or why a tool ran where it did.
Build LobeHub builtin tool packages. Use when adding agent-callable tools, manifests, executors, runtimes, inspectors, renders, placeholders, streaming, interventions, portals, or tool registries.
Build multi-platform chat bots with the chat SDK. Use for Slack, Teams, Google Chat, Discord, GitHub, Linear bots, webhooks, mentions, slash commands, cards, modals, or streaming responses.
>