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