AI
Markdown
Renders a markdown string (GitHub-flavoured: tables, task lists, strikethrough) and stays cheap while an AI reply streams in.
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.
Facts#
| Group | AI |
|---|---|
| Product | Crew OS |
| Global | window.DigitalCrew.Markdown |
| Source | digitalcrew-orchestrator: components/ui/markdown.tsx |
| Import in the app | import { Markdown } from "@/components/ui/markdown"; |
API#
TypeScript declarations emitted from components/ui/markdown.tsx.
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#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<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>