Markdown rendering
End-to-end flow
file.renderMarkdown(tool) reads the file, parses it, and returns metadata.renderMarkdownToTerminalconverts markdown to coloured terminal output.ToolResultPresenterdecides when to render the markdown preview.
Tool implementation
src/tools/FileTools.ts
const renderMarkdown = ({
filePath,
includeHtml,
}: Schema.Schema.Type<typeof RenderMarkdownParameters>) =>
Effect.gen(function* () {
const resolved = yield* pathValidation.ensureWithinCwd(filePath)
const relPath = pathValidation.relativePath(resolved)
const content = yield* fs.readFileString(resolved)
// Handle non-markdown files gracefully
if (!isMarkdownFile(resolved)) {
return createNonMarkdownResult(relPath, content)
}
// Parse markdown and extract structure
const parsed = yield* Effect.promise(() => parseMarkdown(content))
return {
filePath: relPath,
isMarkdown: true,
content: parsed.raw,
plainText: parsed.plainText,
html: includeHtml ? parsed.html : undefined,
metadata: {
headings: parsed.metadata.headings,
links: parsed.metadata.links,
codeBlocks: parsed.metadata.codeBlocks.map((block) => ({
language: block.lang ?? 'text',
lineCount: block.code.split('\n').length,
})),
wordCount: parsed.metadata.wordCount,
lineCount: parsed.metadata.lineCount,
structure: {
headingCount: parsed.metadata.headings.length,
linkCount: parsed.metadata.links.length,
codeBlockCount: parsed.metadata.codeBlocks.length,
},
},
}
})
parseMarkdownreturnsraw,plainText,html, and detailed metadata.- Non-markdown files still return plain content so the assistant can respond gracefully.
Terminal renderer
src/utils/markdown/renderer.ts
// Convert markdown to colored terminal output
export const renderMarkdownToTerminal = (content: string, maxWidth = 80): string => {
const tokens = marked.lexer(content)
return renderTokensToTerminal(tokens, maxWidth)
}
The renderTokensToTerminal function wraps paragraphs, colors headings, formats lists, and prints fenced code blocks with indentation:
src/utils/markdown/renderer.ts
const renderTokensToTerminal = (tokens: Token[], maxWidth: number, indent = ''): string => {
const lines: string[] = []
for (const token of tokens) {
switch (token.type) {
case 'heading':
lines.push(TerminalColors.heading(`${indent}${'#'.repeat(token.depth)} ${token.text}`))
lines.push('')
break
case 'paragraph': {
const wrapped = wrapText(token.text, maxWidth - indent.length)
lines.push(TerminalColors.text(addIndent(wrapped, indent)))
lines.push('')
break
}
case 'list':
lines.push(renderListToTerminal(token as Tokens.List, maxWidth, indent))
lines.push('')
break
case 'code':
lines.push(TerminalColors.codeBlock(`${indent}\`\`\`${token.lang || ''}`))
token.text
.split('\n')
.forEach((codeLine) => lines.push(TerminalColors.code(`${indent}${codeLine}`)))
lines.push(TerminalColors.codeBlock(`${indent}\`\`\``))
lines.push('')
break
// ...other token cases...
}
}
return lines.join('\n').replace(/\n{3,}/g, '\n\n')
}
wrapTextensures paragraphs fit withinmaxWidth.- List and blockquote helpers add bullet/quote prefixes with consistent colouring.
- Additional token handling (links, emphasis, horizontal rules) lives in the same function.
Display trigger
src/chat/ui/ToolResultPresenter.ts
if (
toolName === 'readFile' &&
result?.content &&
result.filePath?.toLowerCase().endsWith('.md') &&
result.content.length < 2000
) {
const rendered = renderMarkdownToTerminal(result.content, 76)
console.log(UI.indent(rendered, 2))
}
- Large files are skipped to avoid flooding the terminal.
renderMarkdownmetadata is still available even when the preview is suppressed.
Customisation ideas
- Adjust
maxWidthto match your terminal size. - Swap
chalkcolours inTerminalColorsto match light/dark themes. - Extend the tool to include HTML output (
includeHtml: true) for downstream processors.
Source
src/tools/FileTools.tssrc/utils/markdown/index.tssrc/utils/markdown/renderer.tssrc/chat/ui/ToolResultPresenter.ts