Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Rich Document Actions

ASecurity

RichDocument action toolkit for markdown and interactive content outside live chat. Use when adding an action or content source, wiring a remote action surface, putting actions on a MarkdownStream/BasicMarkdownContent preview, or touching features/rich-document/**, <RichDocument>, <RichDocumentActionSurface>, registerAction, ContentSourceAdapter, or enableContextMenu. NOT for the live chat message bar (use overlay-system).

3 stars
0 votes
0 copies
1 views
Added 10/3/2026
ai-agentsgoreactexpressawstestingapi

Works with

cursorcliapi

Security Analysis

A100/100

Scanned 10/3/2026

$npx -y skills add armanisadeghi/ai-matrx --skill rich-document-actions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Rich Document Actions?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Rich Document Actions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/armanisadeghi-rich-document-actions/badge)](https://www.skillsdirectory.com/skills/armanisadeghi-rich-document-actions)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: rich-document-actions
description: "RichDocument action toolkit for markdown and interactive content outside live chat. Use when adding an action or content source, wiring a remote action surface, putting actions on a MarkdownStream/BasicMarkdownContent preview, or touching features/rich-document/**, <RichDocument>, <RichDocumentActionSurface>, registerAction, ContentSourceAdapter, or enableContextMenu. NOT for the live chat message bar (use overlay-system)."
---

# RichDocument Actions

Canonical how-to for the **RichDocument** system — the wrapper that pairs the markdown content engine with a configurable, pluggable **action toolkit**. Deep reference: [`features/rich-document/FEATURE.md`](../../../features/rich-document/FEATURE.md). Master design/plan: `~/.claude/plans/if-you-review-the-snappy-stallman.md`.

---

## The most important context

**`MarkdownStream` / `BasicMarkdownContent` are NOT a thin react-markdown wrapper.** They are a multi-thousand-line content engine that renders interactive flashcards (with AI integrations), live diagrams, wired task lists, code surfaces, tool-call visualizations, realtime feeds, classification analyzers, plan viewers, and more. The "markdown" name is historical. **Never reimplement it, never "replace it with a plugin," never fork it.** RichDocument _wraps_ it — it does not replace it.

**RichDocument's job is the action layer**, not rendering. It forwards content to the engine and adds a surface of actions (copy / save-to-notes / save-to-task / print / html-preview / edit / …) that used to exist only on the chat `AssistantActionBar`. The whole point: every surface that shows content can now offer the same depth of interaction with one component.

---

## Mental model — three layers + two extras

```
<RichDocument>                       ← Layer 1: wrapper. Forwards to the engine,
  ├─ MarkdownStream (the engine)        renders the chosen action variant.
  └─ action variant (bar / menu / …)
        │ looks up handlers by id
        ▼
  actions/registry.ts                ← Layer 2: the action registry. Module-scope
  actions/handlers/*.ts                 Map<id, RichDocumentAction>, populated by
                                        self-registering handler modules.
        │ source-specific edit/delete
        ▼
  actions/sources/*.ts               ← per-source adapters (ContentSourceAdapter)

<RichDocumentActionSurface           ← Layer 3 (REMOTE): renders a RichDocument's
   surfaceId="…" />                     actions somewhere else in the tree (a header,
                                        a sidebar) — connected by surfaceId.

enableContextMenu                    ← Extra: lazy, streaming-safe right-click menu.
runtime/providerBridge.ts            ← Extra: module-scope bridge that lets the remote
                                        surface invoke handlers WITHOUT functions in Redux.
```

**Load-bearing invariant:** the `richDocumentActionSurfaces` Redux slice stores **only pure metadata** (action ids, labels, icon names, disabled flags) — never handlers, content, callbacks, or React elements. Handlers live in the module-scope registry and are looked up by id at click time; live content is read through the `providerBridge`'s `getCtx()` getter. This is the same pattern the overlay system uses (`callbackManager`). **Do not put functions in the slice.**

---

## TASK: Use RichDocument on a page (the common case)

Swap a bare `<MarkdownStream content={x}/>` for:

```tsx
import { RichDocument } from "@/features/rich-document/RichDocument";
import type { ContentSource } from "@/features/rich-document/types";

<RichDocument
  content={text}
  source={{ type: "note", noteId }} // drives action visibility + save-to-task linking
  actionsVariant="bar" // bar | mini-bar | menu | icon-only | remote | none
/>;
```

`RichDocument` is `"use client"` (the engine is `dynamic({ ssr:false })`). **Server components cannot render it directly** — render it from a client child.

### Pick a `source`

The discriminated union in `features/rich-document/types.ts`. The type drives which actions show and how `save-to-task` links a parent:

| Source                                                                  | Use for                                                                                                                                |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `{ type: "chat-message", messageId, conversationId, streamRequestId? }` | a cx_message                                                                                                                           |
| `{ type: "note", noteId }`                                              | a note                                                                                                                                 |
| `{ type: "prompt-result", executionId, promptId? }`                     | a prompt run result                                                                                                                    |
| `{ type: "artifact", artifactId }`                                      | an artifact                                                                                                                            |
| `{ type: "scraper-result", runId }`                                     | a scraper/research run                                                                                                                 |
| `{ type: "working-document", conversationId, kind, documentId? }`       | the per-conversation working doc / scratchpad (`kind: "working" \| "scratch"`); edits persist via `persistWorkingDocumentContentThunk` |
| `{ type: "raw" }`                                                       | generic content with no entity link (most read-only previews)                                                                          |

`raw` is fine for read-only displays — you still get copy/save/print/html-preview/etc.; only the parent link on `save-to-task` is absent.

### Pick a variant + position + behavior (three orthogonal axes)

```tsx
actionsVariant: "bar" | "mini-bar" | "menu" | "icon-only" | "remote" | "none"; // WHAT
actionsPosition: "below" |
  "above" |
  "top-right" |
  "top-left" |
  "middle-right" |
  "middle-left"; // WHERE (default "below")
actionsBehavior: "always" | "hover-only"; // VISIBILITY (default "always")
```

Conventions used across the codebase:

- **Main content output** (note preview, research report) → `variant="bar"` (or `"mini-bar"`), `position="below"`, `behavior="always"`.
- **Compact card / preview / overlay** → `variant="icon-only"`, `position="top-right"`, `behavior="hover-only"` (an unobtrusive ⋯ that fades in on hover).
- **Tight footprint** (toast) → `variant="mini-bar"`.

There is **no `"hover-menu"` variant** — it was removed. Express it as `{ icon-only, top-right, hover-only }`.

### Tune which actions appear

All built-ins are included by default; trim/extend via the `actions` prop:

```tsx
actions={{
  exclude: ["announcements", "preferences"],   // hide specific ids
  extra: [ /* custom RichDocumentAction[] — see below */ ],
  callbacks: { onFullPrint, onRequestDelete },  // host-supplied hooks some actions call
}}
```

Source-incompatible actions hide themselves automatically (e.g. `fork-at-message` only shows for `chat-message`). The overflow menu is a **multi-layer** menu (top-level promoted items + Save / Copy as / Export / Edit / Creator / Admin / App submenus, mobile = drawer + accordion) driven by `variants/shared/menuStructure.ts`.

### Add a right-click context menu (optional)

```tsx
enableContextMenu                                    // boolean, or:
enableContextMenu={{ extra: [...], exclude: [...] }} // context-menu-only actions
```

Lazy (the menu chunk loads only on first right-click) and **streaming-safe** (yields to the native browser menu while `isStreamActive`). This is the extension point for future per-surface right-click functionality.

---

## TASK: Render actions in a _different_ location (remote surface)

Put the bar in a page header / toolbar while the content lives elsewhere:

```tsx
// In the header chrome:
<RichDocumentActionSurface surfaceId={`note-detail-${noteId}`} variant="bar" fallback={null} />

// In the body:
<RichDocument
  content={text}
  source={{ type: "note", noteId }}
  actionsVariant="remote"
  actionsSurfaceId={`note-detail-${noteId}`}
/>
```

- Use a **per-entity surfaceId** (`note-detail-${id}`) so fast A→B navigation never collides.
- The surface renders nothing (`fallback`) when no provider is registered (e.g. the body is in a non-preview mode) — so an empty header row is fine.
- The registry is a **stack**: if two RichDocuments target one surfaceId, the most-recently-mounted wins, and out-of-order unmount during navigation stays correct. Don't add your own last-wins logic.
- Real example to copy: `packages/chat/src/agents/components/working-document/WorkingDocumentPanel.tsx` (headless provider + header surface). The live `/notes` header intentionally has no remote consumer, so `NotesView` omits `actionsSurfaceId` and keeps preview/split actions inline.

**Surface draws its OWN content (an editor)?** Don't mount a hidden `RichDocument` just to register the toolbar — it would double-render the heavy engine. Use the **headless `RichDocumentActionProvider`** (renders `null`):

```tsx
// Mounted once, always (gate on whatever "active" means for your surface):
<RichDocumentActionProvider content={liveText} source={mySource} surfaceId={mySurfaceId} />
// The toolbar, anywhere:
<RichDocumentActionSurface surfaceId={mySurfaceId} variant="bar" fallback={null} />
```

This is how the toolbar appears in **every** editor mode (plain / split / wysiwyg / preview), not just the one mode that mounts a `RichDocument`. Real example: `packages/chat/src/agents/components/working-document/WorkingDocumentPanel.tsx` (provider + header bar) + `WorkingDocumentControls.tsx` (compact `menu` surface). It shares `useActionSurfaceProvider` with `RichDocument`, so behavior never drifts.

`MatrxSplit` has opt-in passthrough props (`actionsSource` / `actionsVariant` / `actionsPosition` / `actionsBehavior` / `actionsSurfaceId` / `actionsExclude`) — when `actionsSource` is set it swaps its preview pane to RichDocument (lazily), including the streaming-safe right-click menu. Pattern in `components/matrx/MatrxSplit.tsx`.

---

## TASK: Add a new action

1. Pick the right handler module under `features/rich-document/actions/handlers/` (`copy.ts`, `save.ts`, `export.ts`, `print.ts`, `edit.ts`, `feedback.ts`, `creator.ts`, `fullscreen-editor.ts`, `stubs.ts`, `app.ts`, `server-api.ts`) or add a new one and import it in `actions/handlers/index.ts`.
2. Call `registerAction` at module load:

```ts
import { registerAction } from "../registry";

registerAction({
  id: "my-action", // unique; built-ins are in RichDocumentActionId
  label: "Do the thing", // string OR (ctx) => string for dynamic labels
  icon: SomeLucideIcon,
  iconColor: "text-blue-500 dark:text-blue-400", // optional, preserves visual variety
  category: "save", // feedback|copy|export|save|edit|share|creator|app|admin
  supportedSources: "*", // or ["chat-message", "note", ...]
  renderSlot: "overflow", // "primary" (inline) | "overflow" (⋯) | "both"
  order: 5,
  visible: (ctx) => true, // optional
  disabled: (ctx) => false, // optional; return { reason } for a tooltip
  run: async (ctx) => {
    // ctx: content, source, metadata, dispatch, isAuthenticated, isAdmin,
    //      isCreator, surfaceKey, instanceKey(prefix), sourceAdapter, callbacks, extensions
    await doSomething(ctx.content);
    toast.success("Done"); // handlers OWN their toasts/dialogs
  },
});
```

3. **Place it in the menu hierarchy:** add its id to a section in `features/rich-document/variants/shared/menuStructure.ts` (top-level array for promoted items, or a submenu's `actionIds`). If you skip this, it falls into a trailing "extras" group.
4. **Source-specific behavior goes through the adapter**, never hard-coded: call `ctx.sourceAdapter.edit({ newContent, source, dispatch })` rather than dispatching a chat thunk directly. That's how one generic `edit` action works for chat (editMessage), notes (NotesAPI.update), etc.
5. **Auth-gated actions:** call `requireAuth(ctx, key, featureName, description)` from `actions/utils.ts` at the top of `run`; it opens the authGate overlay and returns false when signed out.
6. Overlay `instanceId`s: use `ctx.instanceKey("prefix")` so two documents on one page don't collide.

For a per-surface, app-specific action, prefer `actions={{ extra: [...] }}` at the call site over a global registry entry.

### `extra` actions that open an overlay

```ts
run: ({ dispatch, source, instanceKey }) =>
  dispatch(openOverlay({
    overlayId: "noteWindow",                 // MUST already exist in features/overlays/
    instanceId: instanceKey("note-window"),
    data: { noteId: (source as Extract<ContentSource,{type:"note"}>).noteId },
  })),
```

The shape is `{ overlayId, data, instanceId }` — **not** `{ component, props }`. To add a _new_ overlay, follow `features/overlays/FEATURE.md` (the 3-file process). Invoke the `overlay-system` skill.

---

## TASK: Add a new content source type

1. Add the variant to `ContentSource` (and, if it carries chat-style baggage, `SourceExtensions`) in `features/rich-document/types.ts`.
2. Create `actions/sources/<type>.ts` implementing `ContentSourceAdapter` — at minimum `instanceKeyPrefix(source)`; add `edit` / `delete` / `reRun` if the source supports them (they gate the `edit` / `delete` actions' visibility).
3. Register it in `actions/sources/index.ts` (`SOURCE_ADAPTERS` map).
4. If `save-to-task` should link a parent, add the source to `sourceToEntityType()` in `actions/handlers/save.ts`.
5. Add the new `type` to the `RichDocumentActionId` source-compatibility expectations in any actions that should support it (`supportedSources`).

---

## Invariants & gotchas

- **No functions / content / React elements in the `richDocumentActionSurfaces` slice.** Metadata only. (Bridge + module registry hold the live stuff.)
- **`"use client"` is mandatory** on anything rendering RichDocument; the engine is SSR-disabled.
- **No `useMemo` / `useCallback` / `React.memo`** — the React Compiler is on. Refs are read only inside event handlers / effects, never during render (`react-hooks/refs`). Avoid impure calls (`Date.now()`) the `react-hooks/purity` rule flags.
- **`renderer` has no `"auto"`.** The basic vs configurable engines preprocess differently; silent swaps diverge output. Default is the standard engine path; pick explicitly only when needed.
- **Do NOT wrap engine-internal block renderers** (`ArtifactBlock`, `MarkdownPreviewBlock`, `StructuredPlanViewer`, anything in `components/mardown-display/`) in RichDocument — they run _inside_ the engine, so wrapping recurses infinitely. The parent message/surface provides actions at the outer level.
- **`MarkdownRenderer`** (`components/ai/MarkdownRenderer` and `components/mardown-display/MarkdownRenderer`) is a SEPARATE lightweight react-markdown primitive, not the heavy engine — out of scope; don't migrate those to RichDocument reflexively.
- **The live chat bar is off-limits here.** `AssistantActionBar` + `messageActionRegistry.ts` (1.6k lines) still power chat and carry intricate chat-only behavior (fork-vs-delete dialog, local thumbs state, density hover, group aggregation, the per-conversation menu gate). RichDocument's handlers were _ported_ from it and are equivalent for `chat-message`, but swapping the live bar is a supervised consolidation (plan Phase 4), not a casual change. Until then the duplication is intentional.

---

## When NOT to use RichDocument

- Engine-internal blocks → recursion (see above).
- A surface that already owns a bespoke action bar you must keep (e.g. `research/DocumentViewer`'s `ContentActionBar`) → swapping changes behavior; leave it or migrate deliberately.
- Pure non-interactive micro-text where a menu would be noise.
- Live streaming chat messages → that's the chat pipeline's territory.

---

## Verification

- `pnpm type-check` and `pnpm eslint features/rich-document/ <your-files>` — must be clean (the feature itself carries zero errors; pre-existing react-compiler warnings in unrelated files are not yours to fix here).
- Browser: render the surface, open the ⋯ menu — confirm the multi-layer hierarchy, that source-incompatible actions are hidden, and that a submenu expands. For a **remote** surface: confirm the body is chrome-free and the header bar opens a menu that operates on the body's live content. For **context menu**: DevTools Network shows no context-menu chunk until first right-click; during an active stream the native browser menu shows instead.
- Auto-login for testing (nonce handshake, two steps): `openssl rand -hex 16 > .dev-login-nonce`, then `/api/dev-login?nonce=<that value>&next=/<route>`.

---

## File map

```
features/rich-document/
├── RichDocument.tsx                 ← the wrapper (engine + variant switch, positioning, context-menu mount; delegates registration to useActionSurfaceProvider)
├── RichDocumentActionSurface.tsx    ← remote renderer (reads slice top-of-stack → bridge → variant)
├── RichDocumentActionProvider.tsx   ← headless: registers the toolkit for a surfaceId WITHOUT the engine (renders null)
├── types.ts                         ← ContentSource, RichDocumentAction(Context), variant/position/behavior, adapter, specs
├── actions/
│   ├── registry.ts                  ← registerAction / getAction / resolveActions
│   ├── utils.ts                     ← requireAuth, getErrorMessage, extractFirstCodeBlock, buildTaskTitle, resolveActionLabel
│   ├── handlers/*.ts                ← self-registering action modules (index.ts imports them all)
│   └── sources/*.ts                 ← per-source ContentSourceAdapter + SOURCE_ADAPTERS map
├── variants/
│   ├── ActionBar / MiniActionBar / MenuVariant   ← inline variants
│   ├── OverflowMenu.tsx             ← ⋯ dropdown (desktop) → MobileActionDrawer (mobile)
│   ├── MobileActionDrawer.tsx       ← bottom sheet + accordion
│   ├── ContextMenu.tsx              ← lazy right-click menu (controlled dropdown at cursor)
│   └── shared/{menuStructure,DropdownMenuTree,PrimaryButtons,runAction,categories}.ts
├── runtime/
│   ├── useActionSurfaceProvider.ts   ← shared registration brain (RichDocument + RichDocumentActionProvider; buildContext/specs/provider+bridge effects)
│   ├── providerBridge.ts            ← module-scope getCtx/actions registry (no functions in Redux)
│   └── ContextMenuMount.tsx         ← lightweight, streaming-safe, lazy-loads ContextMenu
└── redux/actionSurfacesSlice.ts     ← surfaceId → provider stack (metadata only)
```

Real migrated consumers to copy from: `features/notes/components/NoteEditorCore.tsx` + `NotesView.tsx` (inline preview/split actions), `features/tool-call-visualization/renderers/web-research/WebResearchOverlay.tsx` (icon-only hover), and `packages/chat/src/agents/components/working-document/WorkingDocumentPanel.tsx` (headless provider + remote header toolbar in every editor mode).

Attribution

armanisadeghiarmanisadeghi
View sourceSee grades on GitHubMore from armanisadeghi →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Terse caveman voice: answer first, fluff gone, every technical fact kept. Use for /caveman, "caveman mode", "talk like caveman", "be brief", "less tokens". Stays on until "stop caveman" or "normal mode".

1100021 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

698621 votes

Writing Skills

Create and manage Claude Code skills in HASH repository following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns), UserPromptSubmit hook, and the 500-line rule. Includes validation and debugging with SKILL_DEBUG. Examples include rust-error-stack, cargo-dep...

3931 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3421 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Amp, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Grok Build, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

741 votes
View all in ai-agents →