08 → Markdown rendering
Overview
What you'll build
You’re adding a file.renderMarkdown tool that understands docs—not just as plain text, but with structure (headings, links, code blocks) and a terminal-friendly rendering. The assistant can now skim docs, summarize sections, and link to metadata without opening them manually.
Why it matters
- Markdown is the dominant format for documentation in repos; tooling should treat it as first-class.
- Structured metadata helps the agent jump to specific sections or code examples.
- Rendering into the terminal at a fixed width keeps output readable during streaming (Step 05).
Big picture
This sits on top of the File tools stack from Steps 03 and 07. Search finds the file, preview ensures edits are safe, and renderMarkdown shows the doc in a consistent format. Later, Step 11 will talk about performance tweaks you can apply if rendering becomes heavy.
Core concepts (Effect-TS)
Non-Markdown Fallback
Binary files, JSON, or any non-Markdown content should return plain text plus simple statistics. The tool shouldn’t throw—just mark isMarkdown: false and keep going.
Metadata Extraction
parseMarkdown (implemented in src/utils/markdown/index.ts) returns structured data—headings, links, code blocks, counts. Surface those so the assistant can reason about docs without rendering the entire file.
HTML Optionality
Rendering HTML can be expensive and unnecessary for terminal output. Make it opt-in via includeHtml; otherwise keep responses lightweight.
Implementation
Implementation
Step 1: Handle non-Markdown content gracefully
const createNonMarkdownResult = (
filePath: string,
content: string,
): Schema.Schema.Type<typeof RenderMarkdownSuccess> => ({
filePath,
isMarkdown: false,
content,
plainText: content,
metadata: {
headings: [],
links: [],
codeBlocks: [],
wordCount: content.split(/\s+/).length,
lineCount: content.split('\n').length,
structure: {
headingCount: 0,
linkCount: 0,
codeBlockCount: 0,
},
},
})
Step 2: Parse Markdown and return metadata
const renderMarkdown = (params: Schema.Schema.Type<typeof RenderMarkdownParameters>) =>
Effect.gen(function* () {
const { filePath, includeHtml } = params
const resolved = yield* pathValidation.ensureWithinCwd(filePath)
const relPath = pathValidation.relativePath(resolved)
const content = yield* fs.readFileString(resolved)
if (!isMarkdownFile(resolved)) {
return createNonMarkdownResult(relPath, content)
}
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,
},
},
} as const
})
Step 3: Render Markdown to the terminal
export const renderMarkdownToTerminal = (content: string, maxWidth = 80): string => {
const tokens = marked.lexer(content)
return renderTokensToTerminal(tokens, maxWidth)
}
In MessageService you already pass renderMarkdownToTerminal the streaming response (Step 05). Here, you’re preparing the same renderer for file previews.
Performance Notes
- Large docs: consider short-circuiting when
content.lengthexceeds a few hundred KB and return a warning instead of full render. - HTML output: only include
parsed.htmlwhen you explicitly need to hand it to a browser or external renderer. - Parsing cost:
parseMarkdownruns synchronously; if you render huge files frequently, cache results by file hash inFileKeyValueStore.
Testing & Validation
- Render a Markdown doc (
README.md) and ensure headings, links, and code blocks show up in metadata. - Render a plain text file (
package.json); the tool should reportisMarkdown: falseand skip metadata. - Call
renderMarkdownwithincludeHtml: trueand confirm the HTML payload is returned. - Try a file outside the project root;
PathValidationshould fail before reading.
Common Issues
| Problem | Likely Cause | Fix |
|---|---|---|
| Rendering is slow for large files | Rendering full HTML/text for big docs | Short-circuit with a warning or paginate the output |
Special characters print as ? | Terminal locale lacks Unicode support | Use UTF-8 encoding or strip styling in renderMarkdownToTerminal |
| Code blocks lose indentation | Terminal width too narrow | Increase maxWidth or use monospace-friendly wrapping |
| Metadata missing headings | parseMarkdown not recognizing front-matter | Ensure the parser is configured with front-matter plugins if needed |
Connections
Builds on:
- 03 — First Tool — Uses
PathValidationand file access - 05 — Streaming Output — Shares the terminal renderer
Sets up:
- 09 — Session Persistence — Stores rendered messages in history
- 11 — Performance & Concurrency — Optimizes heavy rendering workloads
Related code:
src/tools/FileTools.tssrc/utils/markdown/index.tssrc/utils/markdown/renderer.tssrc/chat/MessageService.ts