Messages and code
The renderers the thread draws text with, for a message, a note or a block of output outside it.
import { Markdown, MessageMarkdown, ReplyMarkdown } from '@adecore/agents-react/chat/ui/Markdown';import { Markdown, MessageMarkdown, ReplyMarkdown } from '@adecore/agents-react/chat/ui/Markdown';
const REPLY = `The retry lives in \`fetchJson\`:
\`\`\`ts
await wait(250 * 2 ** attempt);
\`\`\`
| Status | Retried |
| --- | --- |
| 503 | Yes |
| 404 | No |`;
export default function MarkdownDemo() {
return (
<div className="flex w-full max-w-xl flex-col gap-4">
<MessageMarkdown text={'Review @src/http.ts with $review\nand keep the <Reading> type.'} mentions={['src/http.ts']} skills={['review']} />
<ReplyMarkdown text={REPLY} streaming={false} />
<Markdown text={'A note from the app,\nwith the line break kept.'} breaks fileLinks={false} />
</div>
);
}Markdown
Three renderers, for three kinds of text. All of them write GitHub-flavored Markdown with the typography plugin's prose styles, and none of them renders raw HTML.
MessageMarkdown({ text, mentions?, skills? })is what a person sent. A line break is a line break, a tag typed without backticks stays text, and the files and skills picked in the composer are chips again.ReplyMarkdown({ text, streaming, arriving? })is a reply. It parses a block at a time, so a streamed word only parses the block that grows. Whilestreaming, new words fade in and a code fence that is still open is highlighted as it grows;arrivingfades in each block instead.Markdown({ text, breaks?, fileLinks?, rehypePlugins?, componentOverrides? })is any other text, such as a note from your app.breakskeeps line breaks, andfileLinks(on by default) turns paths into links through the host'sfileLinks.
A path becomes a link only when the host's fileLinks.target(text, cwd) finds one in it. FileLinkContext gives the folder a relative path counts from; the thread provides the chat's own cwd.
CodeBlock
A fenced block, highlighted with Shiki in the theme the host's code names for the current mode. The grammar and the theme load on first use, and the block holds its shape until they are there, so it never flashes uncolored. An unknown language stays plain text.
import { CodeBlock } from '@adecore/agents-react/chat/ui/CodeBlock';
const CODE = `export async function fetchJson<T>(url: string, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
const response = await fetch(url);
if (response.ok) {
return response.json() as Promise<T>;
}
}
}`;
export default function CodeBlockDemo() {
return (
<div className="w-full max-w-xl">
<CodeBlock code={CODE} lang="ts" />
</div>
);
}<CodeBlock code={source} lang="ts" />useCodeTheme() answers the theme id in use. CodeStreamingContext tells a block that its fence is still open, which ReplyMarkdown sets.
AnsiOutput and FadingWords
AnsiOutput({ text, limit?, tailLines? }) draws the output of a command with its colors, bold, italics and underlines. Every other control sequence, cursor moves and links included, is dropped rather than shown. tailLines keeps the last lines and limit the first characters, with a line saying how many were left out. The colors come from the --term-ansi-* tokens; see CSS.
FadingWords({ text }) fades in each word as text grows, the effect a streamed reply has.
import { useEffect, useState } from 'react';
import { AnsiOutput } from '@adecore/agents-react/chat/ui/AnsiOutput';
import { FadingWords } from '@adecore/agents-react/chat/ui/FadingWords';
const OUTPUT =
'\u001b[1mbun test\u001b[0m v1.2.0\n\u001b[32m✓\u001b[0m returns the parsed body\n\u001b[32m✓\u001b[0m retries a 503 twice\n\u001b[31m✗\u001b[0m throws on a 404 at once\n\n \u001b[32m2 pass\u001b[0m\n \u001b[31m1 fail\u001b[0m';
const SENTENCE = 'Each word fades in as it arrives, the way a streamed reply does.';
export default function AnsiOutputDemo() {
const [shown, setShown] = useState(0);
const words = SENTENCE.split(' ');
useEffect(() => {
const timer = setInterval(() => setShown((count) => (count >= words.length + 6 ? 0 : count + 1)), 180);
return () => clearInterval(timer);
}, [words.length]);
return (
<div className="flex w-full max-w-xl flex-col gap-4">
<pre className="overflow-x-auto rounded-md bg-surface-sunken px-3 py-2 font-mono text-xs whitespace-pre-wrap">
<AnsiOutput text={OUTPUT} />
</pre>
<p className="min-h-6 text-sm">
<FadingWords text={words.slice(0, shown).join(' ')} />
</p>
</div>
);
}parseAnsi(text, options) and stripAnsi(text) are the parsing on its own. wordSegments, rehypeFadeWords, rehypeChips and remarkHtmlAsText are the plugins the renderers are built from, and splitMarkdownBlocks the block splitting ReplyMarkdown does.