Skip to main content

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​

src/tools/FileTools.ts
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​

src/tools/FileTools.ts
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​

src/utils/markdown/renderer.ts
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.length exceeds a few hundred KB and return a warning instead of full render.
  • HTML output: only include parsed.html when you explicitly need to hand it to a browser or external renderer.
  • Parsing cost: parseMarkdown runs synchronously; if you render huge files frequently, cache results by file hash in FileKeyValueStore.

Testing & Validation
  1. Render a Markdown doc (README.md) and ensure headings, links, and code blocks show up in metadata.
  2. Render a plain text file (package.json); the tool should report isMarkdown: false and skip metadata.
  3. Call renderMarkdown with includeHtml: true and confirm the HTML payload is returned.
  4. Try a file outside the project root; PathValidation should fail before reading.

Common Issues
ProblemLikely CauseFix
Rendering is slow for large filesRendering full HTML/text for big docsShort-circuit with a warning or paginate the output
Special characters print as ?Terminal locale lacks Unicode supportUse UTF-8 encoding or strip styling in renderMarkdownToTerminal
Code blocks lose indentationTerminal width too narrowIncrease maxWidth or use monospace-friendly wrapping
Metadata missing headingsparseMarkdown not recognizing front-matterEnsure the parser is configured with front-matter plugins if needed

Connections

Builds on:

Sets up:

Related code:

  • src/tools/FileTools.ts
  • src/utils/markdown/index.ts
  • src/utils/markdown/renderer.ts
  • src/chat/MessageService.ts