# Markdown

> Renders a markdown string (GitHub-flavoured: tables, task lists, strikethrough) and stays cheap while an AI reply streams in.

## Facts

- **Product**: Crew OS
- **Family**: AI
- **Global**: `window.DigitalCrew.Markdown`
- **Source**: `digitalcrew-orchestrator: components/ui/markdown.tsx`
- **Import in the app**: `import { Markdown } from "@/components/ui/markdown";`
- **Live preview**: /design/components/markdown/preview.html
- **Page**: /design/components/markdown

## Guidelines

**Use it for** agent replies, task descriptions, comments and notes: anywhere text arrives as markdown.

**What you provide**: `children` (the markdown string), `className` for the wrapper, `id` (a stable key prefix; one is generated otherwise), and `components` to override react-markdown renderers (Crew OS passes its own for chat and blog). Keep `components` stable across renders.

**Anatomy**: the component is headless. Headings, lists and tables arrive unstyled (Tailwind preflight), so style them from `className`; Crew OS task pages use `space-y-2 text-sm [&_a]:text-primary [&_a]:underline`. Inline code is a `font-mono text-sm` span with `px-1`, `rounded-sm`, on `bg-primary-foreground`. Fenced code becomes a code block: `rounded-xl`, 1px `border-border`, `bg-card`, 13px text, 16px padding, horizontal scroll. The fence's `language-*` class picks the grammar (default `plaintext`).

**Behaviour**: the text is split into top-level blocks with `marked`; only the last block is re-parsed as a stream grows, and unchanged blocks keep their memoised render. Code is highlighted with shiki (`github-light`) 120ms after the fence stops changing; the plain `<pre>` shows until then. In this design system shiki is stubbed, so code renders unhighlighted.

**Don't** pass untrusted HTML expecting it to render (raw HTML is not enabled), or restyle blocks with inline styles in the string.

## API

```ts
export type MarkdownProps = {
    children: string;
    id?: string;
    className?: string;
    components?: Partial<Components>;
};
export type MarkdownBlockCache = {
    text: string;
    blocks: string[];
};
/**
 * The block list for `text`, re-reading only what can still change.
 *
 * Streaming text grows a few characters at a time, and re-lexing the whole document
 * on each of those is quadratic over a reply. Every block but the last is already
 * terminated, so only the tail is re-read — and the untouched blocks keep their
 * string identity, which is what lets the per-block memo below bail out.
 *
 * The last block is always re-read rather than kept, because it is the one an
 * addition can still change in place (a paragraph becoming a heading or a table, a
 * list gaining an item, a fence closing).
 */
export declare function blocksFor(text: string, previous: MarkdownBlockCache): string[];
declare const Markdown: any;
export { Markdown };
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  var BT = String.fromCharCode(96), FENCE = BT + BT + BT;
  var md = [
    "## Weekly pipeline brief",
    "",
    "Max booked **4 meetings** this week. Two need a follow-up from you:",
    "",
    "- Claire Dubois (CFO, Northwind Traders) asked for the security pack",
    "- Omar Haddad (VP Finance, Brightline) wants a call after 14 October",
    "- Lena Fischer is back from leave: sequence resumed",
    "",
    "| Campaign | Sent | Replies | Meetings |",
    "| --- | ---: | ---: | ---: |",
    "| Q3 CFO outreach | 1,284 | 97 | 11 |",
    "| Fintech EMEA | 642 | 38 | 4 |",
    "",
    "Pause a sequence with " + BT + "crew pause --campaign q3-cfo" + BT + ", or from the API:",
    "",
    FENCE + "ts",
    "await crew.campaigns.pause(\"q3-cfo-outreach\", {",
    "  reason: \"budget review\",",
    "});",
    FENCE
  ].join("\n");
  var cls = "max-w-2xl space-y-3 text-sm [&_a]:text-primary [&_a]:underline [&_h2]:text-base [&_h2]:font-semibold [&_strong]:font-semibold [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5 [&_table]:w-full [&_table]:border-collapse [&_th]:border-b [&_th]:py-1.5 [&_th]:pr-4 [&_th]:text-left [&_th]:font-medium [&_td]:border-b [&_td]:py-1.5 [&_td]:pr-4 [&_td]:tabular-nums";
  function App() { return h(DC.Markdown, { className: cls }, md); }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in AI

- [Reasoning](/design/components/reasoning.md): A collapsible block that shows how an agent reached its answer, with the reasoning streamed in as markdown.
- [ResponseStream](/design/components/response-stream.md): Reveals text progressively, either typed out character by character or faded in word by word, from a string or an async stream of chunks.
